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 +1 -1
- package/dist/tools/tasks.js +48 -1
- package/dist/utils/TodoistClient.d.ts +6 -0
- package/dist/utils/TodoistClient.js +25 -0
- package/dist/utils/handlers.js +31 -7
- package/dist/utils/version.d.ts +1 -1
- package/dist/utils/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="https://
|
|
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>
|
package/dist/tools/tasks.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/utils/handlers.js
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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}',
|
|
177
|
+
finalPath = `${options.basePath}${options.pathSuffix.replace('{id}', safeItemId)}`;
|
|
156
178
|
}
|
|
157
179
|
else if (options.path) {
|
|
158
|
-
finalPath = options.path.replace('{id}',
|
|
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
|
-
//
|
|
289
|
-
const
|
|
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(),
|
package/dist/utils/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "1.2.
|
|
1
|
+
export declare const version = "1.2.4";
|
package/dist/utils/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated file, do not edit
|
|
2
|
-
export const version = '1.2.
|
|
2
|
+
export const version = '1.2.4';
|