todoist-mcp 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img src="https://static-00.iconduck.com/assets.00/todoist-icon-512x512-v3a6dxo9.png" width="120"/>
2
+ <img src="https://img.icons8.com/color/200/todoist.png" width="120"/>
3
3
  <h1>Todoist MCP Server</h1>
4
4
  <p>A Model Context Protocol (MCP) server implementation that integrates Claude and other AI assistants with Todoist, enabling natural language task management.</p>
5
5
  <div>
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { createApiHandler, createBatchApiHandler, createSyncApiHandler, } from '../utils/handlers.js';
3
+ import { createHandler } from '../utils/handlers.js';
4
+ import { todoistApi } from '../clients.js';
3
5
  /// Common fields for create and update tasks
4
6
  const create_fields = {
5
7
  content: z
@@ -15,7 +17,10 @@ const create_fields = {
15
17
  .string()
16
18
  .optional()
17
19
  .describe('Human defined task due date (ex.: "next Monday", "Tomorrow"). Value is set using local (not UTC) time, if not in english provided, due_lang should be set to the language of the string'),
18
- due_date: z.string().optional().describe('Date in YYYY-MM-DD format relative to user timezone'),
20
+ due_date: z
21
+ .string()
22
+ .optional()
23
+ .describe('Due date in YYYY-MM-DD format relative to user timezone (when you plan to work on task)'),
19
24
  due_datetime: z.string().optional().describe('Specific date and time in RFC3339 format in UTC'),
20
25
  due_lang: z
21
26
  .string()
@@ -35,6 +40,11 @@ const create_fields = {
35
40
  .enum(['minute', 'day'])
36
41
  .optional()
37
42
  .describe('The unit of time that the duration field represents. Must be either minute or day'),
43
+ deadline_date: z
44
+ .string()
45
+ .optional()
46
+ .describe('Deadline date in YYYY-MM-DD format relative to user timezone (fixed date when task must be completed, for tasks with external consequences)'),
47
+ deadline_lang: z.string().optional().describe('2-letter code specifying language of deadline'),
38
48
  };
39
49
  createApiHandler({
40
50
  name: 'get_tasks_list',
@@ -181,3 +191,40 @@ createSyncApiHandler({
181
191
  return { valid: true };
182
192
  },
183
193
  });
194
+ createHandler('get_completed_tasks', 'Get completed tasks from Todoist with filtering options', {
195
+ project_id: z.string().optional().describe('Filter by specific project ID'),
196
+ section_id: z.string().optional().describe('Filter by specific section ID'),
197
+ parent_id: z.string().optional().describe('Filter by specific parent task ID'),
198
+ since: z
199
+ .string()
200
+ .optional()
201
+ .describe('Return tasks completed since this date (YYYY-MM-DD format)'),
202
+ until: z
203
+ .string()
204
+ .optional()
205
+ .describe('Return tasks completed until this date (YYYY-MM-DD format)'),
206
+ limit: z
207
+ .number()
208
+ .int()
209
+ .min(1)
210
+ .max(200)
211
+ .optional()
212
+ .default(50)
213
+ .describe('Number of tasks to return (max 200)'),
214
+ offset: z.number().int().min(0).optional().describe('Offset for pagination'),
215
+ annotation_type: z.string().optional().describe('Filter by annotation type'),
216
+ }, async (args) => {
217
+ const params = {};
218
+ Object.entries(args).forEach(([key, value]) => {
219
+ if (value !== undefined) {
220
+ params[key] = typeof value === 'number' ? value.toString() : value;
221
+ }
222
+ });
223
+ const response = await todoistApi.getCompletedTasks(params);
224
+ return {
225
+ completed_tasks: response.items || [],
226
+ projects: response.projects || {},
227
+ sections: response.sections || {},
228
+ total: response.items?.length || 0,
229
+ };
230
+ });
@@ -30,4 +30,10 @@ export declare class TodoistClient {
30
30
  * @returns API response data
31
31
  */
32
32
  sync(commands: Array<SyncCommand>): Promise<any>;
33
+ /**
34
+ * Get completed tasks using Sync API
35
+ * @param params - Query parameters for filtering completed tasks
36
+ * @returns API response data with completed tasks
37
+ */
38
+ getCompletedTasks(params?: Record<string, string>): Promise<any>;
33
39
  }
@@ -100,4 +100,29 @@ export class TodoistClient {
100
100
  });
101
101
  return this.handleResponse(response);
102
102
  }
103
+ /**
104
+ * Get completed tasks using Sync API
105
+ * @param params - Query parameters for filtering completed tasks
106
+ * @returns API response data with completed tasks
107
+ */
108
+ async getCompletedTasks(params = {}) {
109
+ const url = `${API_SYNC_BASE_URL}/completed/get_all`;
110
+ log(`Making completed tasks request to: ${url} with params:`, JSON.stringify(params, null, 2));
111
+ const formData = new URLSearchParams();
112
+ for (const [key, value] of Object.entries(params)) {
113
+ if (value) {
114
+ formData.append(key, value);
115
+ }
116
+ }
117
+ const response = await fetch(url, {
118
+ method: 'POST',
119
+ headers: {
120
+ Authorization: `Bearer ${this.apiToken}`,
121
+ 'Content-Type': 'application/x-www-form-urlencoded',
122
+ 'X-Request-Id': uuidv4(),
123
+ },
124
+ body: formData.toString(),
125
+ });
126
+ return this.handleResponse(response);
127
+ }
103
128
  }
@@ -3,6 +3,25 @@ import { v4 as uuidv4 } from 'uuid';
3
3
  import { z } from 'zod';
4
4
  import { server, todoistApi } from '../clients.js';
5
5
  import { log } from './helpers.js';
6
+ /**
7
+ * Validates path parameters to prevent path traversal attacks
8
+ * @param value - The parameter value to validate
9
+ * @param paramName - The name of the parameter (for error messages)
10
+ * @returns The URL-encoded safe value
11
+ * @throws Error if validation fails
12
+ */
13
+ function validatePathParameter(value, paramName) {
14
+ const stringValue = String(value);
15
+ // Check for path traversal characters
16
+ if (/[/\\]|\.\./.test(stringValue)) {
17
+ throw new Error(`Invalid characters in path parameter '${paramName}': Path traversal characters are not allowed`);
18
+ }
19
+ // Ensure only safe alphanumeric characters, underscores, and hyphens
20
+ if (!/^[a-zA-Z0-9_-]+$/.test(stringValue)) {
21
+ throw new Error(`Invalid path parameter '${paramName}': Only alphanumeric characters, underscores, and hyphens are allowed`);
22
+ }
23
+ return encodeURIComponent(stringValue);
24
+ }
6
25
  export function createHandler(name, description, paramsSchema, handler) {
7
26
  const mcpToolCallback = async (args) => {
8
27
  try {
@@ -46,9 +65,10 @@ export function createApiHandler(options) {
46
65
  if (args[paramName] === undefined) {
47
66
  throw new Error(`Path parameter ${paramName} is required but not provided`);
48
67
  }
49
- const paramValue = String(args[paramName]);
50
- finalPath = finalPath.replace(fullMatch, paramValue);
51
- pathParams[paramName] = paramValue;
68
+ // Validate and encode path parameter using the centralized security function
69
+ const safeParamValue = validatePathParameter(args[paramName], paramName);
70
+ finalPath = finalPath.replace(fullMatch, safeParamValue);
71
+ pathParams[paramName] = String(args[paramName]);
52
72
  }
53
73
  // Collect non-path parameters for query string or request body
54
74
  const otherParams = Object.entries(args).reduce((acc, [key, value]) => {
@@ -151,11 +171,13 @@ export function createBatchApiHandler(options) {
151
171
  item,
152
172
  };
153
173
  }
174
+ // Apply security validation to itemId before using in path
175
+ const safeItemId = validatePathParameter(itemId, options.idField || 'id');
154
176
  if (options.basePath && options.pathSuffix) {
155
- finalPath = `${options.basePath}${options.pathSuffix.replace('{id}', itemId)}`;
177
+ finalPath = `${options.basePath}${options.pathSuffix.replace('{id}', safeItemId)}`;
156
178
  }
157
179
  else if (options.path) {
158
- finalPath = options.path.replace('{id}', itemId);
180
+ finalPath = options.path.replace('{id}', safeItemId);
159
181
  }
160
182
  delete apiParams[options.idField];
161
183
  if (options.nameField) {
@@ -285,8 +307,10 @@ export function createSyncApiHandler(options) {
285
307
  });
286
308
  continue;
287
309
  }
288
- // Use the provided function to build command args
289
- const commandArgs = options.buildCommandArgs(item, itemId);
310
+ // Apply security validation to itemId before using in sync commands
311
+ const safeItemId = validatePathParameter(itemId, options.idField);
312
+ // Use the provided function to build command args with validated ID
313
+ const commandArgs = options.buildCommandArgs(item, safeItemId);
290
314
  commands.push({
291
315
  type: options.commandType,
292
316
  uuid: uuidv4(),
@@ -1 +1 @@
1
- export declare const version = "1.2.2";
1
+ export declare const version = "1.2.4";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated file, do not edit
2
- export const version = '1.2.2';
2
+ export const version = '1.2.4';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "todoist-mcp",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Todoist MCP Server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",