Todoist MCP Server

by Chrusic
Verified
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { TodoistApi } from "@doist/todoist-api-typescript"; // Task tools const CREATE_TASK_TOOL: Tool = { name: "todoist_create_task", description: "Create one or more tasks in Todoist with full parameter support", inputSchema: { type: "object", properties: { tasks: { type: "array", description: "Array of tasks to create (for batch operations)", items: { type: "object", properties: { content: { type: "string", description: "The content/title of the task (required)" }, description: { type: "string", description: "Detailed description of the task (optional)" }, project_id: { type: "string", description: "ID of the project to add the task to (optional)" }, section_id: { type: "string", description: "ID of the section to add the task to (optional)" }, parent_id: { type: "string", description: "ID of the parent task for subtasks (optional)" }, order: { type: "number", description: "Position in the project or parent task (optional)" }, labels: { type: "array", items: { type: "string" }, description: "Array of label names to apply to the task (optional)" }, priority: { type: "number", description: "Task priority from 1 (normal) to 4 (urgent) (optional)", enum: [1, 2, 3, 4] }, due_string: { type: "string", description: "Natural language due date like 'tomorrow', 'next Monday' (optional)" }, due_date: { type: "string", description: "Due date in YYYY-MM-DD format (optional)" }, due_datetime: { type: "string", description: "Due date and time in RFC3339 format (optional)" }, due_lang: { type: "string", description: "2-letter language code for due date parsing (optional)" }, assignee_id: { type: "string", description: "User ID to assign the task to (optional)" }, duration: { type: "number", description: "The duration amount of the task (optional)" }, duration_unit: { type: "string", description: "The duration unit ('minute' or 'day') (optional)", enum: ["minute", "day"] }, deadline_date: { type: "string", description: "Deadline date in YYYY-MM-DD format (optional)" }, deadline_lang: { type: "string", description: "2-letter language code for deadline parsing (optional)" } }, required: ["content"] } }, // For backward compatibility - single task parameters content: { type: "string", description: "The content/title of the task (for single task creation)" }, description: { type: "string", description: "Detailed description of the task (optional)" }, project_id: { type: "string", description: "ID of the project to add the task to (optional)" }, section_id: { type: "string", description: "ID of the section to add the task to (optional)" }, parent_id: { type: "string", description: "ID of the parent task for subtasks (optional)" }, order: { type: "number", description: "Position in the project or parent task (optional)" }, labels: { type: "array", items: { type: "string" }, description: "Array of label names to apply to the task (optional)" }, priority: { type: "number", description: "Task priority from 1 (normal) to 4 (urgent) (optional)", enum: [1, 2, 3, 4] }, due_string: { type: "string", description: "Natural language due date like 'tomorrow', 'next Monday' (optional)" }, due_date: { type: "string", description: "Due date in YYYY-MM-DD format (optional)" }, due_datetime: { type: "string", description: "Due date and time in RFC3339 format (optional)" }, due_lang: { type: "string", description: "2-letter language code for due date parsing (optional)" }, assignee_id: { type: "string", description: "User ID to assign the task to (optional)" }, duration: { type: "number", description: "The duration amount of the task (optional)" }, duration_unit: { type: "string", description: "The duration unit ('minute' or 'day') (optional)", enum: ["minute", "day"] }, deadline_date: { type: "string", description: "Deadline date in YYYY-MM-DD format (optional)" }, deadline_lang: { type: "string", description: "2-letter language code for deadline parsing (optional)" } } } }; const GET_TASKS_TOOL: Tool = { name: "todoist_get_tasks", description: "Get a list of tasks from Todoist with various filters - handles both single and batch retrieval", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "Filter tasks by project ID (optional)" }, section_id: { type: "string", description: "Filter tasks by section ID (optional)" }, label: { type: "string", description: "Filter tasks by label name (optional)" }, filter: { type: "string", description: "Natural language filter like 'today', 'tomorrow', 'next week', 'priority 1', 'overdue' (optional)" }, lang: { type: "string", description: "IETF language tag defining what language filter is written in (optional)" }, ids: { type: "array", items: { type: "string" }, description: "Array of specific task IDs to retrieve (optional)" }, priority: { type: "number", description: "Filter by priority level (1-4) (optional)", enum: [1, 2, 3, 4] }, limit: { type: "number", description: "Maximum number of tasks to return (optional, client-side filtering)", default: 10 } } } }; const UPDATE_TASK_TOOL: Tool = { name: "todoist_update_task", description: "Update one or more tasks in Todoist with full parameter support", inputSchema: { type: "object", properties: { tasks: { type: "array", description: "Array of tasks to update (for batch operations)", items: { type: "object", properties: { task_id: { type: "string", description: "ID of the task to update (preferred)" }, task_name: { type: "string", description: "Name/content of the task to search for (if ID not provided)" }, content: { type: "string", description: "New content/title for the task (optional)" }, description: { type: "string", description: "New description for the task (optional)" }, project_id: { type: "string", description: "Move task to this project ID (optional)" }, section_id: { type: "string", description: "Move task to this section ID (optional)" }, labels: { type: "array", items: { type: "string" }, description: "New array of label names for the task (optional)" }, priority: { type: "number", description: "New priority level from 1 (normal) to 4 (urgent) (optional)", enum: [1, 2, 3, 4] }, due_string: { type: "string", description: "New due date in natural language (optional)" }, due_date: { type: "string", description: "New due date in YYYY-MM-DD format (optional)" }, due_datetime: { type: "string", description: "New due date and time in RFC3339 format (optional)" }, due_lang: { type: "string", description: "2-letter language code for due date parsing (optional)" }, assignee_id: { type: "string", description: "New user ID to assign the task to (optional)" }, duration: { type: "number", description: "New duration amount of the task (optional)" }, duration_unit: { type: "string", description: "New duration unit ('minute' or 'day') (optional)", enum: ["minute", "day"] }, deadline_date: { type: "string", description: "New deadline date in YYYY-MM-DD format (optional)" }, deadline_lang: { type: "string", description: "2-letter language code for deadline parsing (optional)" } }, anyOf: [ { required: ["task_id"] }, { required: ["task_name"] } ] } }, // For backward compatibility - single task parameters task_id: { type: "string", description: "ID of the task to update (preferred)" }, task_name: { type: "string", description: "Name/content of the task to search for (if ID not provided)" }, content: { type: "string", description: "New content/title for the task (optional)" }, description: { type: "string", description: "New description for the task (optional)" }, project_id: { type: "string", description: "Move task to this project ID (optional)" }, section_id: { type: "string", description: "Move task to this section ID (optional)" }, labels: { type: "array", items: { type: "string" }, description: "New array of label names for the task (optional)" }, priority: { type: "number", description: "New priority level from 1 (normal) to 4 (urgent) (optional)", enum: [1, 2, 3, 4] }, due_string: { type: "string", description: "New due date in natural language (optional)" }, due_date: { type: "string", description: "New due date in YYYY-MM-DD format (optional)" }, due_datetime: { type: "string", description: "New due date and time in RFC3339 format (optional)" }, due_lang: { type: "string", description: "2-letter language code for due date parsing (optional)" }, assignee_id: { type: "string", description: "New user ID to assign the task to (optional)" }, duration: { type: "number", description: "New duration amount of the task (optional)" }, duration_unit: { type: "string", description: "New duration unit ('minute' or 'day') (optional)", enum: ["minute", "day"] }, deadline_date: { type: "string", description: "New deadline date in YYYY-MM-DD format (optional)" }, deadline_lang: { type: "string", description: "2-letter language code for deadline parsing (optional)" } }, anyOf: [ { required: ["tasks"] }, { required: ["task_id"] }, { required: ["task_name"] } ] } }; const DELETE_TASK_TOOL: Tool = { name: "todoist_delete_task", description: "Delete one or more tasks from Todoist", inputSchema: { type: "object", properties: { tasks: { type: "array", description: "Array of tasks to delete (for batch operations)", items: { type: "object", properties: { task_id: { type: "string", description: "ID of the task to delete (preferred)" }, task_name: { type: "string", description: "Name/content of the task to search for and delete (if ID not provided)" } }, anyOf: [ { required: ["task_id"] }, { required: ["task_name"] } ] } }, // For backward compatibility - single task parameters task_id: { type: "string", description: "ID of the task to delete (preferred)" }, task_name: { type: "string", description: "Name/content of the task to search for and delete (if ID not provided)" } }, anyOf: [ { required: ["tasks"] }, { required: ["task_id"] }, { required: ["task_name"] } ] } }; const COMPLETE_TASK_TOOL: Tool = { name: "todoist_complete_task", description: "Mark one or more tasks as complete in Todoist", inputSchema: { type: "object", properties: { tasks: { type: "array", description: "Array of tasks to mark as complete (for batch operations)", items: { type: "object", properties: { task_id: { type: "string", description: "ID of the task to complete (preferred)" }, task_name: { type: "string", description: "Name/content of the task to search for and complete (if ID not provided)" } }, anyOf: [ { required: ["task_id"] }, { required: ["task_name"] } ] } }, // For backward compatibility - single task parameters task_id: { type: "string", description: "ID of the task to complete (preferred)" }, task_name: { type: "string", description: "Name/content of the task to search for and complete (if ID not provided)" } }, anyOf: [ { required: ["tasks"] }, { required: ["task_id"] }, { required: ["task_name"] } ] } }; // Project Tools const GET_PROJECTS_TOOL: Tool = { name: "todoist_get_projects", description: "Get projects with optional filtering and hierarchy information", inputSchema: { type: "object", properties: { project_ids: { type: "array", items: { type: "string" }, description: "Optional: Specific project IDs to retrieve" }, include_sections: { type: "boolean", description: "Optional: Include sections within each project", default: false }, include_hierarchy: { type: "boolean", description: "Optional: Include full parent-child relationships", default: false } } } }; const CREATE_PROJECT_TOOL: Tool = { name: "todoist_create_project", description: "Create one or more projects with support for nested hierarchies", inputSchema: { type: "object", properties: { projects: { type: "array", description: "Array of projects to create (for batch operations)", items: { type: "object", properties: { name: { type: "string", description: "Name of the project" }, parent_id: { type: "string", description: "Parent project ID (optional)" }, parent_name: { type: "string", description: "Name of the parent project (will be created or found automatically)" }, color: { type: "string", description: "Color of the project (optional)", enum: ["berry_red", "red", "orange", "yellow", "olive_green", "lime_green", "green", "mint_green", "teal", "sky_blue", "light_blue", "blue", "grape", "violet", "lavender", "magenta", "salmon", "charcoal", "grey", "taupe"] }, favorite: { type: "boolean", description: "Whether the project is a favorite (optional)" }, view_style: { type: "string", description: "View style of the project (optional)", enum: ["list", "board"] }, sections: { type: "array", items: { type: "string" }, description: "Sections to create within this project (optional)" } }, required: ["name"] } }, // For backward compatibility - single project parameters name: { type: "string", description: "Name of the project (for single project creation)" }, parent_id: { type: "string", description: "Parent project ID (optional)" }, color: { type: "string", description: "Color of the project (optional)", enum: ["berry_red", "red", "orange", "yellow", "olive_green", "lime_green", "green", "mint_green", "teal", "sky_blue", "light_blue", "blue", "grape", "violet", "lavender", "magenta", "salmon", "charcoal", "grey", "taupe"] }, favorite: { type: "boolean", description: "Whether the project is a favorite (optional)" }, view_style: { type: "string", description: "View style of the project (optional)", enum: ["list", "board"] } }, anyOf: [ { required: ["projects"] }, { required: ["name"] } ] } }; const UPDATE_PROJECT_TOOL: Tool = { name: "todoist_update_project", description: "Update one or more projects in Todoist", inputSchema: { type: "object", properties: { projects: { type: "array", description: "Array of projects to update (for batch operations)", items: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to update (preferred)" }, project_name: { type: "string", description: "Name of the project to update (if ID not provided)" }, name: { type: "string", description: "New name for the project (optional)" }, color: { type: "string", description: "New color for the project (optional)", enum: ["berry_red", "red", "orange", "yellow", "olive_green", "lime_green", "green", "mint_green", "teal", "sky_blue", "light_blue", "blue", "grape", "violet", "lavender", "magenta", "salmon", "charcoal", "grey", "taupe"] }, favorite: { type: "boolean", description: "Whether the project should be a favorite (optional)" }, view_style: { type: "string", description: "View style of the project (optional)", enum: ["list", "board"] } }, anyOf: [ { required: ["project_id"] }, { required: ["project_name"] } ] } }, // For backward compatibility - single project parameters project_id: { type: "string", description: "ID of the project to update" }, name: { type: "string", description: "New name for the project (optional)" }, color: { type: "string", description: "New color for the project (optional)", enum: ["berry_red", "red", "orange", "yellow", "olive_green", "lime_green", "green", "mint_green", "teal", "sky_blue", "light_blue", "blue", "grape", "violet", "lavender", "magenta", "salmon", "charcoal", "grey", "taupe"] }, favorite: { type: "boolean", description: "Whether the project should be a favorite (optional)" }, view_style: { type: "string", description: "View style of the project (optional)", enum: ["list", "board"] } }, anyOf: [ { required: ["projects"] }, { required: ["project_id"] } ] } }; const GET_PROJECT_SECTIONS_TOOL: Tool = { name: "todoist_get_project_sections", description: "Get sections from one or more projects in Todoist", inputSchema: { type: "object", properties: { projects: { type: "array", description: "Array of projects to get sections from (for batch operations)", items: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to get sections from (preferred)" }, project_name: { type: "string", description: "Name of the project to get sections from (if ID not provided)" } }, anyOf: [ { required: ["project_id"] }, { required: ["project_name"] } ] } }, // For backward compatibility - single project parameter project_id: { type: "string", description: "ID of the project to get sections from" }, project_name: { type: "string", description: "Name of the project to get sections from (if ID not provided)" }, include_empty: { type: "boolean", description: "Whether to include sections with no tasks", default: true } }, anyOf: [ { required: ["projects"] }, { required: ["project_id"] }, { required: ["project_name"] } ] } }; const CREATE_PROJECT_SECTION_TOOL: Tool = { name: "todoist_create_project_section", description: "Create one or more sections in Todoist projects", inputSchema: { type: "object", properties: { sections: { type: "array", description: "Array of sections to create (for batch operations)", items: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to create the section in" }, project_name: { type: "string", description: "Name of the project to create the section in (if ID not provided)" }, name: { type: "string", description: "Name of the section" }, order: { type: "number", description: "Order of the section (optional)" } }, required: ["name"], anyOf: [ { required: ["project_id"] }, { required: ["project_name"] } ] } }, // For backward compatibility - single section parameters project_id: { type: "string", description: "ID of the project" }, name: { type: "string", description: "Name of the section" }, order: { type: "number", description: "Order of the section (optional)" } }, anyOf: [ { required: ["sections"] }, { required: ["project_id", "name"] } ] } }; // Personal Label Tools const GET_PERSONAL_LABELS_TOOL: Tool = { name: "todoist_get_personal_labels", description: "Get all personal labels from Todoist", inputSchema: { type: "object", properties: {} } }; const CREATE_PERSONAL_LABEL_TOOL: Tool = { name: "todoist_create_personal_label", description: "Create one or more personal labels in Todoist", inputSchema: { type: "object", properties: { labels: { type: "array", description: "Array of labels to create (for batch operations)", items: { type: "object", properties: { name: { type: "string", description: "Name of the label" }, color: { type: "string", description: "Color of the label (optional)", enum: ["berry_red", "red", "orange", "yellow", "olive_green", "lime_green", "green", "mint_green", "teal", "sky_blue", "light_blue", "blue", "grape", "violet", "lavender", "magenta", "salmon", "charcoal", "grey", "taupe"] }, order: { type: "number", description: "Order of the label (optional)" }, is_favorite: { type: "boolean", description: "Whether the label is a favorite (optional)" } }, required: ["name"] } }, // For backward compatibility - single label parameters name: { type: "string", description: "Name of the label" }, color: { type: "string", description: "Color of the label (optional)", enum: ["berry_red", "red", "orange", "yellow", "olive_green", "lime_green", "green", "mint_green", "teal", "sky_blue", "light_blue", "blue", "grape", "violet", "lavender", "magenta", "salmon", "charcoal", "grey", "taupe"] }, order: { type: "number", description: "Order of the label (optional)" }, is_favorite: { type: "boolean", description: "Whether the label is a favorite (optional)" } }, anyOf: [ { required: ["labels"] }, { required: ["name"] } ] } }; const GET_PERSONAL_LABEL_TOOL: Tool = { name: "todoist_get_personal_label", description: "Get a personal label by ID", inputSchema: { type: "object", properties: { label_id: { type: "string", description: "ID of the label to retrieve" } }, required: ["label_id"] } }; const UPDATE_PERSONAL_LABEL_TOOL: Tool = { name: "todoist_update_personal_label", description: "Update one or more existing personal labels in Todoist", inputSchema: { type: "object", properties: { labels: { type: "array", description: "Array of labels to update (for batch operations)", items: { type: "object", properties: { label_id: { type: "string", description: "ID of the label to update (preferred)" }, label_name: { type: "string", description: "Name of the label to search for and update (if ID not provided)" }, name: { type: "string", description: "New name for the label (optional)" }, color: { type: "string", description: "New color for the label (optional)", enum: ["berry_red", "red", "orange", "yellow", "olive_green", "lime_green", "green", "mint_green", "teal", "sky_blue", "light_blue", "blue", "grape", "violet", "lavender", "magenta", "salmon", "charcoal", "grey", "taupe"] }, order: { type: "number", description: "New order for the label (optional)" }, is_favorite: { type: "boolean", description: "Whether the label is a favorite (optional)" } }, anyOf: [ { required: ["label_id"] }, { required: ["label_name"] } ] } }, // For backward compatibility - single label parameters label_id: { type: "string", description: "ID of the label to update" }, label_name: { type: "string", description: "Name of the label to search for and update (if ID not provided)" }, name: { type: "string", description: "New name for the label (optional)" }, color: { type: "string", description: "New color for the label (optional)", enum: ["berry_red", "red", "orange", "yellow", "olive_green", "lime_green", "green", "mint_green", "teal", "sky_blue", "light_blue", "blue", "grape", "violet", "lavender", "magenta", "salmon", "charcoal", "grey", "taupe"] }, order: { type: "number", description: "New order for the label (optional)" }, is_favorite: { type: "boolean", description: "Whether the label is a favorite (optional)" } }, anyOf: [ { required: ["labels"] }, { anyOf: [{ required: ["label_id"] }, { required: ["label_name"] }] } ] } }; const DELETE_PERSONAL_LABEL_TOOL: Tool = { name: "todoist_delete_personal_label", description: "Delete a personal label from Todoist", inputSchema: { type: "object", properties: { label_id: { type: "string", description: "ID of the label to delete" } }, required: ["label_id"] } }; // Shared Label Tools const GET_SHARED_LABELS_TOOL: Tool = { name: "todoist_get_shared_labels", description: "Get all shared labels from Todoist", inputSchema: { type: "object", properties: { omit_personal: { type: "boolean", description: "Whether to exclude the names of the user's personal labels from the results (default: false)" } } } }; const RENAME_SHARED_LABELS_TOOL: Tool = { name: "todoist_rename_shared_labels", description: "Rename one or more shared labels in Todoist", inputSchema: { type: "object", properties: { labels: { type: "array", description: "Array of label rename operations (for batch operations)", items: { type: "object", properties: { name: { type: "string", description: "The name of the existing label to rename" }, new_name: { type: "string", description: "The new name for the label" } }, required: ["name", "new_name"] } }, // For backward compatibility - single label parameters name: { type: "string", description: "The name of the existing label to rename" }, new_name: { type: "string", description: "The new name for the label" } }, anyOf: [ { required: ["labels"] }, { required: ["name", "new_name"] } ] } }; const REMOVE_SHARED_LABELS_TOOL: Tool = { name: "todoist_remove_shared_labels", description: "Remove one or more shared labels from Todoist tasks", inputSchema: { type: "object", properties: { labels: { type: "array", description: "Array of shared label names to remove (for batch operations)", items: { type: "object", properties: { name: { type: "string", description: "The name of the label to remove" } }, required: ["name"] } }, // For backward compatibility - single label parameter name: { type: "string", description: "The name of the label to remove" } }, anyOf: [ { required: ["labels"] }, { required: ["name"] } ] } }; // Task Label Tool const UPDATE_TASK_LABELS_TOOL: Tool = { name: "todoist_update_task_labels", description: "Update the labels of one or more tasks in Todoist", inputSchema: { type: "object", properties: { tasks: { type: "array", description: "Array of tasks to update labels for (for batch operations)", items: { type: "object", properties: { task_id: { type: "string", description: "ID of the task to update labels for (preferred)" }, task_name: { type: "string", description: "Name/content of the task to search for and update labels (if ID not provided)" }, labels: { type: "array", items: { type: "string" }, description: "Array of label names to set for the task" } }, required: ["labels"], anyOf: [ { required: ["task_id"] }, { required: ["task_name"] } ] } }, // For backward compatibility - single task parameters task_id: { type: "string", description: "ID of the task to update labels for (preferred)" }, task_name: { type: "string", description: "Name/content of the task to search for and update labels (if ID not provided)" }, labels: { type: "array", items: { type: "string" }, description: "Array of label names to set for the task" } }, anyOf: [ { required: ["tasks"] }, { required: ["labels"], anyOf: [{ required: ["task_id"] }, { required: ["task_name"] }] } ] } }; // Server implementation const server = new Server( { name: "todoist-mcp-server", version: "0.1.0", }, { capabilities: { tools: {}, }, }, ); // Check for API token const TODOIST_API_TOKEN = process.env.TODOIST_API_TOKEN!; if (!TODOIST_API_TOKEN) { console.error("Error: TODOIST_API_TOKEN environment variable is required"); process.exit(1); } // Initialize Todoist client const todoistClient = new TodoistApi(TODOIST_API_TOKEN); // Task Tools TypeGuards function isCreateTaskArgs(args: unknown): args is { content?: string; description?: string; project_id?: string; section_id?: string; parent_id?: string; order?: number; labels?: string[]; priority?: number; due_string?: string; due_date?: string; due_datetime?: string; due_lang?: string; assignee_id?: string; duration?: number; duration_unit?: string; deadline_date?: string; deadline_lang?: string; tasks?: Array<{ content: string; description?: string; project_id?: string; section_id?: string; parent_id?: string; order?: number; labels?: string[]; priority?: number; due_string?: string; due_date?: string; due_datetime?: string; due_lang?: string; assignee_id?: string; duration?: number; duration_unit?: string; deadline_date?: string; deadline_lang?: string; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("tasks" in args && Array.isArray((args as any).tasks)) { return (args as any).tasks.every((task: any) => typeof task === "object" && task !== null && "content" in task && typeof task.content === "string" ); } // Check if it's a single task operation return "content" in args && typeof (args as any).content === "string"; } function isGetTasksArgs(args: unknown): args is { project_id?: string; section_id?: string; label?: string; filter?: string; lang?: string; ids?: string[]; priority?: number; limit?: number; } { return ( typeof args === "object" && args !== null ); } function isUpdateTaskArgs(args: unknown): args is { task_id?: string; task_name?: string; content?: string; description?: string; project_id?: string; section_id?: string; labels?: string[]; priority?: number; due_string?: string; due_date?: string; due_datetime?: string; due_lang?: string; assignee_id?: string; duration?: number; duration_unit?: string; deadline_date?: string; deadline_lang?: string; tasks?: Array<{ task_id?: string; task_name?: string; content?: string; description?: string; project_id?: string; section_id?: string; labels?: string[]; priority?: number; due_string?: string; due_date?: string; due_datetime?: string; due_lang?: string; assignee_id?: string; duration?: number; duration_unit?: string; deadline_date?: string; deadline_lang?: string; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("tasks" in args && Array.isArray((args as any).tasks)) { return (args as any).tasks.every((task: any) => typeof task === "object" && task !== null && (("task_id" in task && typeof task.task_id === "string") || ("task_name" in task && typeof task.task_name === "string")) ); } // Check if it's a single task operation return ( ("task_id" in args && typeof (args as any).task_id === "string") || ("task_name" in args && typeof (args as any).task_name === "string") ); } function isDeleteTaskArgs(args: unknown): args is { task_id?: string; task_name?: string; tasks?: Array<{ task_id?: string; task_name?: string; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("tasks" in args && Array.isArray((args as any).tasks)) { return (args as any).tasks.every((task: any) => typeof task === "object" && task !== null && (("task_id" in task && typeof task.task_id === "string") || ("task_name" in task && typeof task.task_name === "string")) ); } // Check if it's a single task operation return ( ("task_id" in args && typeof (args as any).task_id === "string") || ("task_name" in args && typeof (args as any).task_name === "string") ); } function isCompleteTaskArgs(args: unknown): args is { task_id?: string; task_name?: string; tasks?: Array<{ task_id?: string; task_name?: string; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("tasks" in args && Array.isArray((args as any).tasks)) { return (args as any).tasks.every((task: any) => typeof task === "object" && task !== null && (("task_id" in task && typeof task.task_id === "string") || ("task_name" in task && typeof task.task_name === "string")) ); } // Check if it's a single task operation return ( ("task_id" in args && typeof (args as any).task_id === "string") || ("task_name" in args && typeof (args as any).task_name === "string") ); } // Project Tools TypeGuards function isGetProjectsArgs(args: unknown): args is { project_ids?: string[]; include_sections?: boolean; include_hierarchy?: boolean; } { return ( typeof args === "object" && args !== null ); } function isCreateProjectArgs(args: unknown): args is { name?: string; parent_id?: string; color?: string; favorite?: boolean; view_style?: string; projects?: Array<{ name: string; parent_id?: string; parent_name?: string; color?: string; favorite?: boolean; view_style?: string; sections?: string[]; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("projects" in args && Array.isArray((args as any).projects)) { return (args as any).projects.every((project: any) => typeof project === "object" && project !== null && "name" in project && typeof project.name === "string" ); } // Check if it's a single project operation return "name" in args && typeof (args as any).name === "string"; } function isUpdateProjectArgs(args: unknown): args is { project_id?: string; name?: string; color?: string; favorite?: boolean; view_style?: string; projects?: Array<{ project_id?: string; project_name?: string; name?: string; color?: string; favorite?: boolean; view_style?: string; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("projects" in args && Array.isArray((args as any).projects)) { return (args as any).projects.every((project: any) => typeof project === "object" && project !== null && (("project_id" in project && typeof project.project_id === "string") || ("project_name" in project && typeof project.project_name === "string")) ); } // Check if it's a single project operation return "project_id" in args && typeof (args as any).project_id === "string"; } function isGetProjectSectionsArgs(args: unknown): args is { project_id?: string; project_name?: string; include_empty?: boolean; projects?: Array<{ project_id?: string; project_name?: string; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("projects" in args && Array.isArray((args as any).projects)) { return (args as any).projects.every((project: any) => typeof project === "object" && project !== null && (("project_id" in project && typeof project.project_id === "string") || ("project_name" in project && typeof project.project_name === "string")) ); } // Check if it's a single project operation return ( ("project_id" in args && typeof (args as any).project_id === "string") || ("project_name" in args && typeof (args as any).project_name === "string") ); } function isCreateProjectSectionArgs(args: unknown): args is { project_id?: string; project_name?: string; name?: string; order?: number; sections?: Array<{ project_id?: string; project_name?: string; name: string; order?: number; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("sections" in args && Array.isArray((args as any).sections)) { return (args as any).sections.every((section: any) => typeof section === "object" && section !== null && "name" in section && typeof section.name === "string" && ( (section.project_id === undefined || typeof section.project_id === "string") && (section.project_name === undefined || typeof section.project_name === "string") && (section.order === undefined || typeof section.order === "number") && (section.project_id !== undefined || section.project_name !== undefined) ) ); } // Check if it's a single section operation return ( "project_id" in args && typeof (args as any).project_id === "string" && "name" in args && typeof (args as any).name === "string" && ((args as any).order === undefined || typeof (args as any).order === "number") ); } // Label Tools TypeGuards function isGetPersonalLabelsArgs(args: unknown): args is {} { return typeof args === "object" && args !== null; } function isCreatePersonalLabelArgs(args: unknown): args is { name?: string; color?: string; order?: number; is_favorite?: boolean; labels?: Array<{ name: string; color?: string; order?: number; is_favorite?: boolean; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("labels" in args && Array.isArray((args as any).labels)) { return (args as any).labels.every((label: any) => typeof label === "object" && label !== null && "name" in label && typeof label.name === "string" && (label.color === undefined || typeof label.color === "string") && (label.order === undefined || typeof label.order === "number") && (label.is_favorite === undefined || typeof label.is_favorite === "boolean") ); } // Check if it's a single label operation return ( "name" in args && typeof (args as any).name === "string" && ((args as any).color === undefined || typeof (args as any).color === "string") && ((args as any).order === undefined || typeof (args as any).order === "number") && ((args as any).is_favorite === undefined || typeof (args as any).is_favorite === "boolean") ); } function isGetPersonalLabelArgs(args: unknown): args is { label_id: string; } { return ( typeof args === "object" && args !== null && "label_id" in args && typeof (args as { label_id: string }).label_id === "string" ); } function isUpdatePersonalLabelArgs(args: unknown): args is { label_id?: string; label_name?: string; name?: string; color?: string; order?: number; is_favorite?: boolean; labels?: Array<{ label_id?: string; label_name?: string; name?: string; color?: string; order?: number; is_favorite?: boolean; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("labels" in args && Array.isArray((args as any).labels)) { return (args as any).labels.every((label: any) => typeof label === "object" && label !== null && ( (label.label_id !== undefined && typeof label.label_id === "string") || (label.label_name !== undefined && typeof label.label_name === "string") ) && (label.name === undefined || typeof label.name === "string") && (label.color === undefined || typeof label.color === "string") && (label.order === undefined || typeof label.order === "number") && (label.is_favorite === undefined || typeof label.is_favorite === "boolean") ); } // Check if it's a single label operation return ( ( ("label_id" in args && typeof (args as any).label_id === "string") || ("label_name" in args && typeof (args as any).label_name === "string") ) && ((args as any).name === undefined || typeof (args as any).name === "string") && ((args as any).color === undefined || typeof (args as any).color === "string") && ((args as any).order === undefined || typeof (args as any).order === "number") && ((args as any).is_favorite === undefined || typeof (args as any).is_favorite === "boolean") ); } function isDeletePersonalLabelArgs(args: unknown): args is { label_id: string; } { return ( typeof args === "object" && args !== null && "label_id" in args && typeof (args as { label_id: string }).label_id === "string" ); } // Shared Label Tools Typeguards function isGetSharedLabelsArgs(args: unknown): args is { omit_personal?: boolean; } { if (typeof args !== "object" || args === null) { return false; } return ( !("omit_personal" in args) || typeof (args as any).omit_personal === "boolean" ); } function isRenameSharedLabelsArgs(args: unknown): args is { name?: string; new_name?: string; labels?: Array<{ name: string; new_name: string; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("labels" in args && Array.isArray((args as any).labels)) { return (args as any).labels.every((label: any) => typeof label === "object" && label !== null && "name" in label && typeof label.name === "string" && "new_name" in label && typeof label.new_name === "string" ); } // Check if it's a single label operation return ( "name" in args && typeof (args as any).name === "string" && "new_name" in args && typeof (args as any).new_name === "string" ); } function isRemoveSharedLabelsArgs(args: unknown): args is { name?: string; labels?: Array<{ name: string; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("labels" in args && Array.isArray((args as any).labels)) { return (args as any).labels.every((label: any) => typeof label === "object" && label !== null && "name" in label && typeof label.name === "string" ); } // Check if it's a single label operation return ( "name" in args && typeof (args as any).name === "string" ); } // Task Label Typeguard function isUpdateTaskLabelsArgs(args: unknown): args is { task_id?: string; task_name?: string; labels?: string[]; tasks?: Array<{ task_id?: string; task_name?: string; labels: string[]; }>; } { if (typeof args !== "object" || args === null) { return false; } // Check if it's a batch operation if ("tasks" in args && Array.isArray((args as any).tasks)) { return (args as any).tasks.every((task: any) => typeof task === "object" && task !== null && "labels" in task && Array.isArray(task.labels) && ( (task.task_id === undefined || typeof task.task_id === "string") && (task.task_name === undefined || typeof task.task_name === "string") && (task.task_id !== undefined || task.task_name !== undefined) ) ); } // Check if it's a single task operation return ( "labels" in args && Array.isArray((args as any).labels) && ( (("task_id" in args) && typeof (args as any).task_id === "string") || (("task_name" in args) && typeof (args as any).task_name === "string") ) ); } // List tools Schema server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ CREATE_TASK_TOOL, GET_TASKS_TOOL, UPDATE_TASK_TOOL, DELETE_TASK_TOOL, COMPLETE_TASK_TOOL, GET_PROJECTS_TOOL, CREATE_PROJECT_TOOL, UPDATE_PROJECT_TOOL, GET_PROJECT_SECTIONS_TOOL, CREATE_PROJECT_SECTION_TOOL, GET_PERSONAL_LABELS_TOOL, CREATE_PERSONAL_LABEL_TOOL, GET_PERSONAL_LABEL_TOOL, UPDATE_PERSONAL_LABEL_TOOL, DELETE_PERSONAL_LABEL_TOOL, GET_SHARED_LABELS_TOOL, RENAME_SHARED_LABELS_TOOL, REMOVE_SHARED_LABELS_TOOL, UPDATE_TASK_LABELS_TOOL ], })); // All Tool handlers server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; if (!args) { throw new Error("No arguments provided"); } // Task Handlers if (name === "todoist_create_task") { if (!isCreateTaskArgs(args)) { throw new Error("Invalid arguments for todoist_create_task"); } try { // Handle batch task creation if (args.tasks && args.tasks.length > 0) { const results = await Promise.all(args.tasks.map(async (taskData) => { try { // Map our parameters to the Todoist API format const apiParams: any = { content: taskData.content, description: taskData.description, projectId: taskData.project_id, sectionId: taskData.section_id, parentId: taskData.parent_id, order: taskData.order, labels: taskData.labels, priority: taskData.priority, dueString: taskData.due_string, dueDate: taskData.due_date, dueDateTime: taskData.due_datetime, dueLang: taskData.due_lang, assigneeId: taskData.assignee_id, }; // Handle duration parameters if (taskData.duration && taskData.duration_unit) { apiParams.duration = { amount: taskData.duration, unit: taskData.duration_unit }; } // Handle deadline parameters if (taskData.deadline_date) { apiParams.deadlineDate = taskData.deadline_date; } if (taskData.deadline_lang) { apiParams.deadlineLang = taskData.deadline_lang; } const task = await todoistClient.addTask(apiParams); return { success: true, task }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), taskData }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.tasks.length, summary: { total: args.tasks.length, succeeded: successCount, failed: args.tasks.length - successCount }, results }, null, 2) }], isError: successCount < args.tasks.length }; } // Handle single task creation else if (args.content) { // Map our parameters to the Todoist API format const apiParams: any = { content: args.content, description: args.description, projectId: args.project_id, sectionId: args.section_id, parentId: args.parent_id, order: args.order, labels: args.labels, priority: args.priority, dueString: args.due_string, dueDate: args.due_date, dueDateTime: args.due_datetime, dueLang: args.due_lang, assigneeId: args.assignee_id, }; // Handle duration parameters if (args.duration && args.duration_unit) { apiParams.duration = { amount: args.duration, unit: args.duration_unit }; } // Handle deadline parameters if (args.deadline_date) { apiParams.deadlineDate = args.deadline_date; } if (args.deadline_lang) { apiParams.deadlineLang = args.deadline_lang; } const task = await todoistClient.addTask(apiParams); return { content: [{ type: "text", text: JSON.stringify({ success: true, task }, null, 2) }], isError: false }; } else { throw new Error("Either 'content' or 'tasks' must be provided"); } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } if (name === "todoist_get_tasks") { if (!isGetTasksArgs(args)) { throw new Error("Invalid arguments for todoist_get_tasks"); } try { // Build API request parameters const requestParams: any = {}; if (args.project_id) { requestParams.project_id = args.project_id; } if (args.section_id) { requestParams.section_id = args.section_id; } if (args.label) { requestParams.label = args.label; } if (args.filter) { requestParams.filter = args.filter; } if (args.lang) { requestParams.lang = args.lang; } if (args.ids && args.ids.length > 0) { requestParams.ids = args.ids; } // Get tasks with a single API call using appropriate filters const allTasks = await todoistClient.getTasks(requestParams); // Apply any additional client-side filtering let filteredTasks = allTasks; // Apply priority filter (API doesn't support this directly) if (args.priority) { filteredTasks = filteredTasks.filter(task => task.priority === args.priority); } // Apply limit if (args.limit && args.limit > 0 && filteredTasks.length > args.limit) { filteredTasks = filteredTasks.slice(0, args.limit); } // Format response as JSON for easier LLM parsing return { content: [{ type: "text", text: JSON.stringify({ success: true, tasks: filteredTasks, count: filteredTasks.length }, null, 2) }], isError: false, }; } catch (error) { console.error('Error in todoist_get_tasks:', error); return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true, }; } } if (name === "todoist_update_task") { if (!isUpdateTaskArgs(args)) { throw new Error("Invalid arguments for todoist_update_task"); } try { // Process batch update if (args.tasks && args.tasks.length > 0) { // Get all tasks in one API call to efficiently search by name const allTasks = await todoistClient.getTasks(); const results = await Promise.all(args.tasks.map(async (taskData) => { try { // Determine task ID - either directly provided or find by name let taskId = taskData.task_id; if (!taskId && taskData.task_name) { const matchingTask = allTasks.find(task => task.content.toLowerCase().includes(taskData.task_name!.toLowerCase()) ); if (!matchingTask) { return { success: false, error: `Task not found: ${taskData.task_name}`, taskData }; } taskId = matchingTask.id; } if (!taskId) { return { success: false, error: "Either task_id or task_name must be provided", taskData }; } // Build update parameters const updateData: any = {}; if (taskData.content !== undefined) updateData.content = taskData.content; if (taskData.description !== undefined) updateData.description = taskData.description; if (taskData.project_id !== undefined) updateData.projectId = taskData.project_id; if (taskData.section_id !== undefined) updateData.sectionId = taskData.section_id; if (taskData.labels !== undefined) updateData.labels = taskData.labels; if (taskData.priority !== undefined) updateData.priority = taskData.priority; if (taskData.due_string !== undefined) updateData.dueString = taskData.due_string; if (taskData.due_date !== undefined) updateData.dueDate = taskData.due_date; if (taskData.due_datetime !== undefined) updateData.dueDateTime = taskData.due_datetime; if (taskData.due_lang !== undefined) updateData.dueLang = taskData.due_lang; if (taskData.assignee_id !== undefined) updateData.assigneeId = taskData.assignee_id; // Handle duration if (taskData.duration !== undefined && taskData.duration_unit !== undefined) { updateData.duration = { amount: taskData.duration, unit: taskData.duration_unit }; } else if (taskData.duration === null) { updateData.duration = null; // Remove duration } // Handle deadline if (taskData.deadline_date !== undefined) { updateData.deadlineDate = taskData.deadline_date; } if (taskData.deadline_lang !== undefined) { updateData.deadlineLang = taskData.deadline_lang; } // Perform the update await todoistClient.updateTask(taskId, updateData); return { success: true, taskId: taskId, updated: updateData }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), taskData }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.tasks.length, summary: { total: args.tasks.length, succeeded: successCount, failed: args.tasks.length - successCount }, results }, null, 2) }], isError: successCount < args.tasks.length }; } // Process single task update else { // Determine task ID - either directly provided or find by name let taskId = args.task_id; if (!taskId && args.task_name) { const tasks = await todoistClient.getTasks(); const matchingTask = tasks.find(task => task.content.toLowerCase().includes(args.task_name!.toLowerCase()) ); if (!matchingTask) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Task not found: ${args.task_name}` }, null, 2) }], isError: true }; } taskId = matchingTask.id; } if (!taskId) { throw new Error("Either task_id or task_name must be provided"); } // Build update parameters const updateData: any = {}; if (args.content !== undefined) updateData.content = args.content; if (args.description !== undefined) updateData.description = args.description; if (args.project_id !== undefined) updateData.projectId = args.project_id; if (args.section_id !== undefined) updateData.sectionId = args.section_id; if (args.labels !== undefined) updateData.labels = args.labels; if (args.priority !== undefined) updateData.priority = args.priority; if (args.due_string !== undefined) updateData.dueString = args.due_string; if (args.due_date !== undefined) updateData.dueDate = args.due_date; if (args.due_datetime !== undefined) updateData.dueDateTime = args.due_datetime; if (args.due_lang !== undefined) updateData.dueLang = args.due_lang; if (args.assignee_id !== undefined) updateData.assigneeId = args.assignee_id; // Handle duration if (args.duration !== undefined && args.duration_unit !== undefined) { updateData.duration = { amount: args.duration, unit: args.duration_unit }; } else if (args.duration === null) { updateData.duration = null; // Remove duration } // Handle deadline if (args.deadline_date !== undefined) { updateData.deadlineDate = args.deadline_date; } if (args.deadline_lang !== undefined) { updateData.deadlineLang = args.deadline_lang; } // Perform the update const updatedTask = await todoistClient.updateTask(taskId, updateData); return { content: [{ type: "text", text: JSON.stringify({ success: true, task: updatedTask }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } if (name === "todoist_delete_task") { if (!isDeleteTaskArgs(args)) { throw new Error("Invalid arguments for todoist_delete_task"); } try { // Process batch deletion if (args.tasks && args.tasks.length > 0) { // Get all tasks in one API call to efficiently search by name const allTasks = await todoistClient.getTasks(); const results = await Promise.all(args.tasks.map(async (taskData) => { try { // Determine task ID - either directly provided or find by name let taskId = taskData.task_id; let taskContent = ''; if (!taskId && taskData.task_name) { const matchingTask = allTasks.find(task => task.content.toLowerCase().includes(taskData.task_name!.toLowerCase()) ); if (!matchingTask) { return { success: false, error: `Task not found: ${taskData.task_name}`, task_name: taskData.task_name }; } taskId = matchingTask.id; taskContent = matchingTask.content; } if (!taskId) { return { success: false, error: "Either task_id or task_name must be provided", taskData }; } // Delete the task await todoistClient.deleteTask(taskId); return { success: true, task_id: taskId, content: taskContent || `Task ID: ${taskId}` }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), taskData }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.tasks.length, summary: { total: args.tasks.length, succeeded: successCount, failed: args.tasks.length - successCount }, results }, null, 2) }], isError: successCount < args.tasks.length }; } // Process single task deletion else { // Determine task ID - either directly provided or find by name let taskId = args.task_id; let taskContent = ''; if (!taskId && args.task_name) { const tasks = await todoistClient.getTasks(); const matchingTask = tasks.find(task => task.content.toLowerCase().includes(args.task_name!.toLowerCase()) ); if (!matchingTask) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Task not found: ${args.task_name}` }, null, 2) }], isError: true }; } taskId = matchingTask.id; taskContent = matchingTask.content; } if (!taskId) { throw new Error("Either task_id or task_name must be provided"); } // Delete the task await todoistClient.deleteTask(taskId); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Successfully deleted task${taskContent ? ': "' + taskContent + '"' : ' with ID: ' + taskId}` }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } if (name === "todoist_complete_task") { if (!isCompleteTaskArgs(args)) { throw new Error("Invalid arguments for todoist_complete_task"); } try { // Process batch completion if (args.tasks && args.tasks.length > 0) { // Get all tasks in one API call to efficiently search by name const allTasks = await todoistClient.getTasks(); const results = await Promise.all(args.tasks.map(async (taskData) => { try { // Determine task ID - either directly provided or find by name let taskId = taskData.task_id; let taskContent = ''; if (!taskId && taskData.task_name) { const matchingTask = allTasks.find(task => task.content.toLowerCase().includes(taskData.task_name!.toLowerCase()) ); if (!matchingTask) { return { success: false, error: `Task not found: ${taskData.task_name}`, task_name: taskData.task_name }; } taskId = matchingTask.id; taskContent = matchingTask.content; } if (!taskId) { return { success: false, error: "Either task_id or task_name must be provided", taskData }; } // Complete the task await todoistClient.closeTask(taskId); return { success: true, task_id: taskId, content: taskContent || `Task ID: ${taskId}` }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), taskData }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.tasks.length, summary: { total: args.tasks.length, succeeded: successCount, failed: args.tasks.length - successCount }, results }, null, 2) }], isError: successCount < args.tasks.length }; } // Process single task completion else { // Determine task ID - either directly provided or find by name let taskId = args.task_id; let taskContent = ''; if (!taskId && args.task_name) { const tasks = await todoistClient.getTasks(); const matchingTask = tasks.find(task => task.content.toLowerCase().includes(args.task_name!.toLowerCase()) ); if (!matchingTask) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Task not found: ${args.task_name}` }, null, 2) }], isError: true }; } taskId = matchingTask.id; taskContent = matchingTask.content; } if (!taskId) { throw new Error("Either task_id or task_name must be provided"); } // Complete the task await todoistClient.closeTask(taskId); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Successfully completed task${taskContent ? ': "' + taskContent + '"' : ' with ID: ' + taskId}` }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } // Project Handlers if (name === "todoist_get_projects") { if (!isGetProjectsArgs(args)) { throw new Error("Invalid arguments for todoist_get_projects"); } try { // Get all projects in a single API call const projects = await todoistClient.getProjects(); // Create a response object const response: any = { success: true, projects: projects }; // Handle specific project IDs if provided if (args.project_ids && args.project_ids.length > 0) { response.projects = projects.filter(project => args.project_ids!.includes(project.id) ); } // Add section information if requested if (args.include_sections) { const projectSections = await Promise.all( response.projects.map(async (project: any) => { try { const sections = await todoistClient.getSections(project.id); return { ...project, sections: sections }; } catch (error) { return { ...project, sections: [], sections_error: error instanceof Error ? error.message : String(error) }; } }) ); response.projects = projectSections; } // Add hierarchy information if requested if (args.include_hierarchy) { // Create a map for quick project lookup const projectMap = new Map(); response.projects.forEach((project: any) => { projectMap.set(project.id, { ...project, children: [] }); }); // Build the hierarchy const rootProjects: any[] = []; response.projects.forEach((project: any) => { const projectWithHierarchy = projectMap.get(project.id); if (project.parentId) { const parent = projectMap.get(project.parentId); if (parent) { parent.children.push(projectWithHierarchy); } else { rootProjects.push(projectWithHierarchy); } } else { rootProjects.push(projectWithHierarchy); } }); response.hierarchy = rootProjects; } return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], isError: false, }; } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true, }; } } if (name === "todoist_create_project") { if (!isCreateProjectArgs(args)) { throw new Error("Invalid arguments for todoist_create_project"); } try { // Handle batch project creation if (args.projects && args.projects.length > 0) { // First get all existing projects to handle parent_name references const existingProjects = await todoistClient.getProjects(); const projectNameToIdMap = new Map<string, string>(); existingProjects.forEach(project => { projectNameToIdMap.set(project.name.toLowerCase(), project.id); }); // Keep track of newly created projects as well const newProjectMap = new Map<string, string>(); const results = await Promise.all(args.projects.map(async (projectData) => { try { // Determine parent ID from name or ID let parentId = projectData.parent_id; if (!parentId && projectData.parent_name) { // Look for parent in existing projects parentId = projectNameToIdMap.get(projectData.parent_name.toLowerCase()); // Or look in newly created projects if (!parentId) { parentId = newProjectMap.get(projectData.parent_name.toLowerCase()); } if (!parentId) { return { success: false, error: `Parent project not found: ${projectData.parent_name}`, projectData }; } } // Create the project const projectParams: any = { name: projectData.name, color: projectData.color, viewStyle: projectData.view_style }; if (parentId) { projectParams.parentId = parentId; } if (projectData.favorite !== undefined) { projectParams.isFavorite = projectData.favorite; } const project = await todoistClient.addProject(projectParams); // Save to our map for potential children newProjectMap.set(project.name.toLowerCase(), project.id); // Create sections if specified const sections: Array<any> = []; // Fix: Explicitly define the type if (projectData.sections && projectData.sections.length > 0) { for (const sectionName of projectData.sections) { try { const section = await todoistClient.addSection({ name: sectionName, projectId: project.id }); sections.push(section); } catch (error) { sections.push({ name: sectionName, error: error instanceof Error ? error.message : String(error) }); } } } return { success: true, project, sections: sections.length > 0 ? sections : undefined }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), projectData }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.projects.length, summary: { total: args.projects.length, succeeded: successCount, failed: args.projects.length - successCount }, results }, null, 2) }], isError: successCount < args.projects.length }; } // Handle single project creation (backward compatibility) else { const projectParams: any = { name: args.name, parentId: args.parent_id, color: args.color }; if (args.view_style) { projectParams.viewStyle = args.view_style; } if (args.favorite !== undefined) { projectParams.isFavorite = args.favorite; } const project = await todoistClient.addProject(projectParams); return { content: [{ type: "text", text: JSON.stringify({ success: true, project }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } if (name === "todoist_update_project") { if (!isUpdateProjectArgs(args)) { throw new Error("Invalid arguments for todoist_update_project"); } try { // Handle batch project update if (args.projects && args.projects.length > 0) { // Get all projects to find by name if needed const allProjects = await todoistClient.getProjects(); const results = await Promise.all(args.projects.map(async (projectData) => { try { // Determine project ID - either directly provided or find by name let projectId = projectData.project_id; let projectDetails = null; if (!projectId && projectData.project_name) { const matchingProject = allProjects.find(project => project.name.toLowerCase().includes(projectData.project_name!.toLowerCase()) ); if (!matchingProject) { return { success: false, error: `Project not found: ${projectData.project_name}`, projectData }; } projectId = matchingProject.id; projectDetails = matchingProject; } else if (projectId) { projectDetails = allProjects.find(p => p.id === projectId); } if (!projectId) { return { success: false, error: "Either project_id or project_name must be provided", projectData }; } // Build update parameters const updateData: any = {}; if (projectData.name !== undefined) updateData.name = projectData.name; if (projectData.color !== undefined) updateData.color = projectData.color; if (projectData.favorite !== undefined) updateData.isFavorite = projectData.favorite; if (projectData.view_style !== undefined) updateData.viewStyle = projectData.view_style; // Perform the update const updatedProject = await todoistClient.updateProject(projectId, updateData); return { success: true, project_id: projectId, original_name: projectDetails?.name || "Unknown", updated: updatedProject }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), projectData }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.projects.length, summary: { total: args.projects.length, succeeded: successCount, failed: args.projects.length - successCount }, results }, null, 2) }], isError: successCount < args.projects.length }; } // Process single project update (backward compatibility) else { // Build update data const updateData: any = {}; if (args.name !== undefined) updateData.name = args.name; if (args.color !== undefined) updateData.color = args.color; if (args.favorite !== undefined) updateData.isFavorite = args.favorite; if (args.view_style !== undefined) updateData.viewStyle = args.view_style; // Perform the update const updatedProject = await todoistClient.updateProject(args.project_id!, updateData); return { content: [{ type: "text", text: JSON.stringify({ success: true, project: updatedProject }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } if (name === "todoist_get_project_sections") { if (!isGetProjectSectionsArgs(args)) { throw new Error("Invalid arguments for todoist_get_project_sections"); } try { // Process batch project sections retrieval if (args.projects && args.projects.length > 0) { // Get all projects in one API call to efficiently search by name const allProjects = await todoistClient.getProjects(); const results = await Promise.all(args.projects.map(async (projectData) => { try { // Determine project ID - either directly provided or find by name let projectId = projectData.project_id; let projectName = ""; if (!projectId && projectData.project_name) { const matchingProject = allProjects.find(project => project.name.toLowerCase().includes(projectData.project_name!.toLowerCase()) ); if (!matchingProject) { return { success: false, error: `Project not found: ${projectData.project_name}`, project_name: projectData.project_name }; } projectId = matchingProject.id; projectName = matchingProject.name; } if (!projectId) { return { success: false, error: "Either project_id or project_name must be provided", projectData }; } // Get all sections for this project const sections = await todoistClient.getSections(projectId); // Optionally get tasks for each section to determine if empty let sectionsWithTaskCount = sections; if (args.include_empty === false) { const projectTasks = await todoistClient.getTasks({ projectId }); sectionsWithTaskCount = sections.filter(section => { const sectionTasks = projectTasks.filter(task => task.sectionId === section.id); return sectionTasks.length > 0; }); } return { success: true, project_id: projectId, project_name: projectName || `Project ID: ${projectId}`, sections: sectionsWithTaskCount, count: sectionsWithTaskCount.length }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), projectData }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.projects.length, summary: { total: args.projects.length, succeeded: successCount, failed: args.projects.length - successCount }, results }, null, 2) }], isError: successCount < args.projects.length }; } // Process single project sections retrieval else { // Determine project ID - either directly provided or find by name let projectId = args.project_id; let projectName = ""; if (!projectId && args.project_name) { const projects = await todoistClient.getProjects(); const matchingProject = projects.find(project => project.name.toLowerCase().includes(args.project_name!.toLowerCase()) ); if (!matchingProject) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Project not found: ${args.project_name}` }, null, 2) }], isError: true }; } projectId = matchingProject.id; projectName = matchingProject.name; } if (!projectId) { throw new Error("Either project_id or project_name must be provided"); } // Get all sections for this project const sections = await todoistClient.getSections(projectId); // Optionally filter empty sections let sectionsResult = sections; if (args.include_empty === false) { const projectTasks = await todoistClient.getTasks({ projectId }); sectionsResult = sections.filter(section => { const sectionTasks = projectTasks.filter(task => task.sectionId === section.id); return sectionTasks.length > 0; }); } return { content: [{ type: "text", text: JSON.stringify({ success: true, project_id: projectId, project_name: projectName || undefined, sections: sectionsResult, count: sectionsResult.length }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } if (name === "todoist_create_project_section") { if (!isCreateProjectSectionArgs(args)) { throw new Error("Invalid arguments for todoist_create_project_section"); } try { // Handle batch section creation if (args.sections && args.sections.length > 0) { // Get all projects in one API call to efficiently search by name if needed const allProjects = await todoistClient.getProjects(); const projectNameToIdMap = new Map<string, string>(); allProjects.forEach(project => { projectNameToIdMap.set(project.name.toLowerCase(), project.id); }); const results = await Promise.all(args.sections.map(async (sectionData) => { try { // Determine project ID - either directly provided or find by name let projectId = sectionData.project_id; let projectName = ""; if (!projectId && sectionData.project_name) { const matchingProject = allProjects.find(project => project.name.toLowerCase().includes(sectionData.project_name!.toLowerCase()) ); if (!matchingProject) { return { success: false, error: `Project not found: ${sectionData.project_name}`, section_name: sectionData.name }; } projectId = matchingProject.id; projectName = matchingProject.name; } if (!projectId) { return { success: false, error: "Either project_id or project_name must be provided", section_name: sectionData.name }; } // Create the section const section = await todoistClient.addSection({ projectId: projectId, name: sectionData.name, order: sectionData.order }); return { success: true, section, project_name: projectName || undefined }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), section_data: sectionData }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.sections.length, summary: { total: args.sections.length, succeeded: successCount, failed: args.sections.length - successCount }, results }, null, 2) }], isError: successCount < args.sections.length }; } // Process single section creation (backward compatibility) else { const section = await todoistClient.addSection({ projectId: args.project_id!, name: args.name!, order: args.order }); return { content: [{ type: "text", text: JSON.stringify({ success: true, section }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } // Personal Label Handlers if (name === "todoist_get_personal_labels") { if (!isGetPersonalLabelsArgs(args)) { throw new Error("Invalid arguments for todoist_get_personal_labels"); } const labels = await todoistClient.getLabels(); return { content: [{ type: "text", text: JSON.stringify(labels, null, 2) }], isError: false, }; } if (name === "todoist_create_personal_label") { if (!isCreatePersonalLabelArgs(args)) { throw new Error("Invalid arguments for todoist_create_personal_label"); } try { // Handle batch label creation if (args.labels && args.labels.length > 0) { const results = await Promise.all(args.labels.map(async (labelData) => { try { // Create the label const label = await todoistClient.addLabel({ name: labelData.name, color: labelData.color, order: labelData.order, isFavorite: labelData.is_favorite }); return { success: true, label }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), label_name: labelData.name }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.labels.length, summary: { total: args.labels.length, succeeded: successCount, failed: args.labels.length - successCount }, results }, null, 2) }], isError: successCount < args.labels.length }; } // Handle single label creation (backward compatibility) else { const label = await todoistClient.addLabel({ name: args.name!, color: args.color, order: args.order, isFavorite: args.is_favorite }); return { content: [{ type: "text", text: JSON.stringify({ success: true, label }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } if (name === "todoist_get_personal_label") { if (!isGetPersonalLabelArgs(args)) { throw new Error("Invalid arguments for todoist_get_personal_label"); } const label = await todoistClient.getLabel(args.label_id); return { content: [{ type: "text", text: JSON.stringify(label, null, 2) }], isError: false, }; } if (name === "todoist_update_personal_label") { if (!isUpdatePersonalLabelArgs(args)) { throw new Error("Invalid arguments for todoist_update_personal_label"); } try { // Handle batch label updates if (args.labels && args.labels.length > 0) { // Get all labels in one API call to efficiently search by name const allLabels = await todoistClient.getLabels(); const results = await Promise.all(args.labels.map(async (labelData) => { try { // Determine label ID - either directly provided or find by name let labelId = labelData.label_id; let labelName = ''; if (!labelId && labelData.label_name) { const matchingLabel = allLabels.find(label => label.name.toLowerCase() === labelData.label_name!.toLowerCase() ); if (!matchingLabel) { return { success: false, error: `Label not found: ${labelData.label_name}`, label_name: labelData.label_name }; } labelId = matchingLabel.id; labelName = matchingLabel.name; } if (!labelId) { return { success: false, error: "Either label_id or label_name must be provided", labelData }; } // Build update parameters const updateData: any = {}; if (labelData.name !== undefined) updateData.name = labelData.name; if (labelData.color !== undefined) updateData.color = labelData.color; if (labelData.order !== undefined) updateData.order = labelData.order; if (labelData.is_favorite !== undefined) updateData.isFavorite = labelData.is_favorite; // Update the label const updatedLabel = await todoistClient.updateLabel(labelId, updateData); return { success: true, label: updatedLabel, original_name: labelName || undefined }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), labelData }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.labels.length, summary: { total: args.labels.length, succeeded: successCount, failed: args.labels.length - successCount }, results }, null, 2) }], isError: successCount < args.labels.length }; } // Handle single label update (backward compatibility) else { // Determine label ID - either directly provided or find by name let labelId = args.label_id; if (!labelId && args.label_name) { const labels = await todoistClient.getLabels(); const matchingLabel = labels.find(label => label.name.toLowerCase() === args.label_name!.toLowerCase() ); if (!matchingLabel) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Label not found: ${args.label_name}` }, null, 2) }], isError: true }; } labelId = matchingLabel.id; } if (!labelId) { throw new Error("Either label_id or label_name must be provided"); } // Build update parameters const updateData: any = {}; if (args.name !== undefined) updateData.name = args.name; if (args.color !== undefined) updateData.color = args.color; if (args.order !== undefined) updateData.order = args.order; if (args.is_favorite !== undefined) updateData.isFavorite = args.is_favorite; // Update the label const updatedLabel = await todoistClient.updateLabel(labelId, updateData); return { content: [{ type: "text", text: JSON.stringify({ success: true, label: updatedLabel }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } if (name === "todoist_delete_personal_label") { if (!isDeletePersonalLabelArgs(args)) { throw new Error("Invalid arguments for todoist_delete_personal_label"); } await todoistClient.deleteLabel(args.label_id); return { content: [{ type: "text", text: `Successfully deleted label with ID: ${args.label_id}` }], isError: false, }; } // Shared Label Handlers if (name === "todoist_get_shared_labels") { if (!isGetSharedLabelsArgs(args)) { throw new Error("Invalid arguments for todoist_get_shared_labels"); } try { const sharedLabels = await todoistClient.getSharedLabels({ omitPersonal: args.omit_personal }); return { content: [{ type: "text", text: JSON.stringify({ success: true, shared_labels: sharedLabels, count: sharedLabels.length }, null, 2) }], isError: false }; } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } if (name === "todoist_rename_shared_labels") { if (!isRenameSharedLabelsArgs(args)) { throw new Error("Invalid arguments for todoist_rename_shared_labels"); } try { // Handle batch label renaming if (args.labels && args.labels.length > 0) { const results = await Promise.all(args.labels.map(async (labelData) => { try { // Rename the shared label await todoistClient.renameSharedLabel({ name: labelData.name, newName: labelData.new_name }); return { success: true, old_name: labelData.name, new_name: labelData.new_name }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), label_name: labelData.name }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.labels.length, summary: { total: args.labels.length, succeeded: successCount, failed: args.labels.length - successCount }, results }, null, 2) }], isError: successCount < args.labels.length }; } // Handle single label renaming (backward compatibility) else { await todoistClient.renameSharedLabel({ name: args.name!, newName: args.new_name! }); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Successfully renamed shared label "${args.name}" to "${args.new_name}"` }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } if (name === "todoist_remove_shared_labels") { if (!isRemoveSharedLabelsArgs(args)) { throw new Error("Invalid arguments for todoist_remove_shared_labels"); } try { // Handle batch label removal if (args.labels && args.labels.length > 0) { const results = await Promise.all(args.labels.map(async (labelData) => { try { // Remove the shared label await todoistClient.removeSharedLabel({ name: labelData.name }); return { success: true, label_name: labelData.name }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), label_name: labelData.name }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.labels.length, summary: { total: args.labels.length, succeeded: successCount, failed: args.labels.length - successCount }, results }, null, 2) }], isError: successCount < args.labels.length }; } // Handle single label removal (backward compatibility) else { await todoistClient.removeSharedLabel({ name: args.name! }); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: `Successfully removed all instances of shared label "${args.name}"` }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } // Task Label Handler if (name === "todoist_update_task_labels") { if (!isUpdateTaskLabelsArgs(args)) { throw new Error("Invalid arguments for todoist_update_task_labels"); } try { // Process batch label updates if (args.tasks && args.tasks.length > 0) { // Get all tasks in one API call to efficiently search by name const allTasks = await todoistClient.getTasks(); const results = await Promise.all(args.tasks.map(async (taskData) => { try { // Determine task ID - either directly provided or find by name let taskId = taskData.task_id; let taskContent = ''; if (!taskId && taskData.task_name) { const matchingTask = allTasks.find(task => task.content.toLowerCase().includes(taskData.task_name!.toLowerCase()) ); if (!matchingTask) { return { success: false, error: `Task not found: ${taskData.task_name}`, task_name: taskData.task_name }; } taskId = matchingTask.id; taskContent = matchingTask.content; } if (!taskId) { return { success: false, error: "Either task_id or task_name must be provided", taskData }; } // Update the task labels await todoistClient.updateTask(taskId, { labels: taskData.labels }); return { success: true, task_id: taskId, content: taskContent || `Task ID: ${taskId}`, labels: taskData.labels }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), taskData }; } })); const successCount = results.filter(r => r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: successCount === args.tasks.length, summary: { total: args.tasks.length, succeeded: successCount, failed: args.tasks.length - successCount }, results }, null, 2) }], isError: successCount < args.tasks.length }; } // Process single task label update (backward compatibility) else { // Determine task ID - either directly provided or find by name let taskId = args.task_id; let taskContent = ''; if (!taskId && args.task_name) { const tasks = await todoistClient.getTasks(); const matchingTask = tasks.find(task => task.content.toLowerCase().includes(args.task_name!.toLowerCase()) ); if (!matchingTask) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Task not found: ${args.task_name}` }, null, 2) }], isError: true }; } taskId = matchingTask.id; taskContent = matchingTask.content; } if (!taskId) { throw new Error("Either task_id or task_name must be provided"); } // Update the task labels await todoistClient.updateTask(taskId, { labels: args.labels }); return { content: [{ type: "text", text: JSON.stringify({ success: true, task_id: taskId, content: taskContent || `Task ID: ${taskId}`, labels: args.labels }, null, 2) }], isError: false }; } } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }, null, 2) }], isError: true }; } } return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // async functions async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Todoist MCP Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });