@kydycode/todoist-mcp-server-ext 0.1.0 → 0.2.0

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.
Files changed (3) hide show
  1. package/README.md +1 -5
  2. package/dist/index.js +318 -19
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,14 +1,10 @@
1
1
  # Enhanced Todoist MCP Server Extended
2
- [![smithery badge](https://smithery.ai/badge/@abhiz123/todoist-mcp-server)](https://smithery.ai/server/@abhiz123/todoist-mcp-server)
2
+ [![smithery badge](https://smithery.ai/badge/@kydycode/todoist-mcp-server-ext)](https://smithery.ai/server/@kydycode/todoist-mcp-server-ext)
3
3
 
4
4
  > **Extended Version** - Forked and enhanced by [kydycode](https://github.com/kydycode) from the original [@abhiz123/todoist-mcp-server](https://github.com/abhiz123/todoist-mcp-server)
5
5
 
6
6
  A comprehensive MCP (Model Context Protocol) server implementation that provides full integration between Claude and Todoist. This **extended version** includes additional features, improved compatibility, and enhanced functionality using the complete Todoist API with the latest MCP SDK.
7
7
 
8
- <a href="https://glama.ai/mcp/servers/fhaif4fv1w">
9
- <img width="380" height="200" src="https://glama.ai/mcp/servers/fhaif4fv1w/badge" alt="Todoist Server MCP server" />
10
- </a>
11
-
12
8
  ## 🆕 Extended Version Features
13
9
 
14
10
  ### 🔧 **Technical Improvements**
package/dist/index.js CHANGED
@@ -209,6 +209,104 @@ const REOPEN_TASK_TOOL = {
209
209
  required: ["taskId"]
210
210
  }
211
211
  };
212
+ const MOVE_TASK_TOOL = {
213
+ name: "todoist_move_task",
214
+ description: "Move a task to a different project, section, or make it a subtask of another task. Provide the taskId and exactly one of: projectId, sectionId, or parentId.",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {
218
+ taskId: {
219
+ type: "string",
220
+ description: "The ID of the task to move."
221
+ },
222
+ projectId: {
223
+ type: "string",
224
+ description: "The ID of the destination project. (Optional, use only one of projectId, sectionId, parentId)"
225
+ },
226
+ sectionId: {
227
+ type: "string",
228
+ description: "The ID of the destination section. (Optional, use only one of projectId, sectionId, parentId)"
229
+ },
230
+ parentId: {
231
+ type: "string",
232
+ description: "The ID of the parent task to move this task under. (Optional, use only one of projectId, sectionId, parentId)"
233
+ }
234
+ },
235
+ required: ["taskId"]
236
+ // Note: Validation for providing exactly one of projectId, sectionId, or parentId
237
+ // is handled in the isMoveTaskArgs type guard and the tool handler.
238
+ // A more complex JSON schema with oneOf could enforce this, but client support varies.
239
+ }
240
+ };
241
+ // Label Management Tools
242
+ const CREATE_LABEL_TOOL = {
243
+ name: "todoist_create_label",
244
+ description: "Create a new label.",
245
+ inputSchema: {
246
+ type: "object",
247
+ properties: {
248
+ name: { type: "string", description: "The name of the label." },
249
+ color: { type: "string", description: "Label color name or code (e.g., 'berry_red', '#FF0000') (optional)." },
250
+ isFavorite: { type: "boolean", description: "Whether the label should be a favorite (optional)." },
251
+ order: { type: "number", description: "The order of the label in the list (optional)." }
252
+ },
253
+ required: ["name"]
254
+ }
255
+ };
256
+ const GET_LABEL_TOOL = {
257
+ name: "todoist_get_label",
258
+ description: "Get a specific label by its ID.",
259
+ inputSchema: {
260
+ type: "object",
261
+ properties: {
262
+ labelId: { type: "string", description: "The ID of the label to retrieve." }
263
+ },
264
+ required: ["labelId"]
265
+ }
266
+ };
267
+ const GET_LABELS_TOOL = {
268
+ name: "todoist_get_labels",
269
+ description: "Get all labels. Supports pagination.",
270
+ inputSchema: {
271
+ type: "object",
272
+ properties: {
273
+ cursor: {
274
+ type: "string",
275
+ description: "Pagination cursor for next page (optional)."
276
+ },
277
+ limit: {
278
+ type: "number",
279
+ description: "Maximum number of labels to return (default: 50) (optional)."
280
+ }
281
+ }
282
+ }
283
+ };
284
+ const UPDATE_LABEL_TOOL = {
285
+ name: "todoist_update_label",
286
+ description: "Update an existing label by its ID.",
287
+ inputSchema: {
288
+ type: "object",
289
+ properties: {
290
+ labelId: { type: "string", description: "The ID of the label to update." },
291
+ name: { type: "string", description: "New name for the label (optional)." },
292
+ color: { type: "string", description: "New color for the label (optional)." },
293
+ isFavorite: { type: "boolean", description: "New favorite status (optional)." },
294
+ order: { type: "number", description: "New order for the label (optional)." }
295
+ },
296
+ required: ["labelId"]
297
+ }
298
+ };
299
+ const DELETE_LABEL_TOOL = {
300
+ name: "todoist_delete_label",
301
+ description: "Delete a label by its ID.",
302
+ inputSchema: {
303
+ type: "object",
304
+ properties: {
305
+ labelId: { type: "string", description: "The ID of the label to delete." }
306
+ },
307
+ required: ["labelId"]
308
+ }
309
+ };
212
310
  // Project Management Tools
213
311
  const GET_PROJECTS_TOOL = {
214
312
  name: "todoist_get_projects",
@@ -421,11 +519,38 @@ const server = new Server({
421
519
  });
422
520
  // Helper function to format task output
423
521
  function formatTask(task) {
424
- return `- ${task.content}${task.description ? `\n Description: ${task.description}` : ''}${task.due ? `\n Due: ${task.due.string}` : ''}${task.priority && task.priority > 1 ? `\n Priority: ${task.priority}` : ''}${task.labels && task.labels.length > 0 ? `\n Labels: ${task.labels.join(', ')}` : ''}${task.parentId ? `\n Parent: ${task.parentId}` : ''}`;
522
+ let taskDetails = `- ID: ${task.id}\n Content: ${task.content}`;
523
+ if (task.description)
524
+ taskDetails += `\n Description: ${task.description}`;
525
+ if (task.due)
526
+ taskDetails += `\n Due: ${task.due.string}`;
527
+ if (task.priority && task.priority > 1)
528
+ taskDetails += `\n Priority: ${task.priority}`;
529
+ if (task.labels && task.labels.length > 0)
530
+ taskDetails += `\n Labels: ${task.labels.join(', ')}`;
531
+ if (task.projectId)
532
+ taskDetails += `\n Project ID: ${task.projectId}`;
533
+ if (task.sectionId)
534
+ taskDetails += `\n Section ID: ${task.sectionId}`;
535
+ if (task.parentId)
536
+ taskDetails += `\n Parent ID: ${task.parentId}`;
537
+ if (task.url)
538
+ taskDetails += `\n URL: ${task.url}`;
539
+ if (task.commentCount > 0)
540
+ taskDetails += `\n Comments: ${task.commentCount}`;
541
+ if (task.createdAt)
542
+ taskDetails += `\n Created At: ${task.createdAt}`;
543
+ if (task.creatorId)
544
+ taskDetails += `\n Creator ID: ${task.creatorId}`;
545
+ return taskDetails;
425
546
  }
426
547
  // Helper function to format project output
427
548
  function formatProject(project) {
428
- return `- ${project.name}${project.color ? `\n Color: ${project.color}` : ''}${project.isFavorite ? `\n Favorite: Yes` : ''}${project.viewStyle ? `\n View: ${project.viewStyle}` : ''}${project.parentId ? `\n Parent: ${project.parentId}` : ''}`;
549
+ return `- ${project.name}${project.color ? `\n Color: ${project.color}` : ''}${project.isFavorite ? `\n Favorite: Yes` : ''}${project.viewStyle ? `\n View: ${project.viewStyle}` : ''}${project.parentId ? `\n Parent: ${project.parentId}` : ''}${project.id ? ` (ID: ${project.id})` : ''}`;
550
+ }
551
+ // Helper function to format label output
552
+ function formatLabel(label) {
553
+ return `- ${label.name} (ID: ${label.id})${label.color ? `\n Color: ${label.color}` : ''}${label.isFavorite ? `\n Favorite: Yes` : ''}${label.order ? `\n Order: ${label.order}` : ''}`;
429
554
  }
430
555
  // Type guards for arguments
431
556
  function isCreateTaskArgs(args) {
@@ -456,6 +581,7 @@ function isUpdateTaskArgs(args) {
456
581
  typeof args.taskId === "string");
457
582
  }
458
583
  function isProjectArgs(args) {
584
+ // Allows empty object or object with optional cursor/limit
459
585
  return typeof args === "object" && args !== null;
460
586
  }
461
587
  function isProjectIdArgs(args) {
@@ -507,6 +633,39 @@ function isSearchTasksArgs(args) {
507
633
  "query" in args &&
508
634
  typeof args.query === "string");
509
635
  }
636
+ function isMoveTaskArgs(args) {
637
+ if (typeof args !== 'object' || args === null || !('taskId' in args) || typeof args.taskId !== 'string') {
638
+ return false;
639
+ }
640
+ const { projectId, sectionId, parentId } = args;
641
+ const destinations = [projectId, sectionId, parentId];
642
+ const providedDestinations = destinations.filter(dest => dest !== undefined && dest !== null && String(dest).trim() !== '');
643
+ // Exactly one destination must be provided and be a non-empty string
644
+ return providedDestinations.length === 1 &&
645
+ providedDestinations.every(dest => typeof dest === 'string');
646
+ }
647
+ function isCreateLabelArgs(args) {
648
+ return (typeof args === "object" &&
649
+ args !== null &&
650
+ "name" in args &&
651
+ typeof args.name === "string");
652
+ }
653
+ function isLabelIdArgs(args) {
654
+ return (typeof args === "object" &&
655
+ args !== null &&
656
+ "labelId" in args &&
657
+ typeof args.labelId === "string");
658
+ }
659
+ // Type guard for get_labels which takes no arguments in this SDK version
660
+ function isGetLabelsArgs(args) {
661
+ return typeof args === "object" && args !== null;
662
+ }
663
+ function isUpdateLabelArgs(args) {
664
+ return (typeof args === "object" &&
665
+ args !== null &&
666
+ "labelId" in args &&
667
+ typeof args.labelId === "string");
668
+ }
510
669
  // Tool handlers
511
670
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
512
671
  tools: [
@@ -520,6 +679,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
520
679
  COMPLETE_TASK_TOOL,
521
680
  REOPEN_TASK_TOOL,
522
681
  SEARCH_TASKS_TOOL,
682
+ MOVE_TASK_TOOL,
523
683
  // Project tools
524
684
  GET_PROJECTS_TOOL,
525
685
  GET_PROJECT_TOOL,
@@ -531,6 +691,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
531
691
  CREATE_SECTION_TOOL,
532
692
  UPDATE_SECTION_TOOL,
533
693
  DELETE_SECTION_TOOL,
694
+ // Label tools
695
+ CREATE_LABEL_TOOL,
696
+ GET_LABEL_TOOL,
697
+ GET_LABELS_TOOL,
698
+ UPDATE_LABEL_TOOL,
699
+ DELETE_LABEL_TOOL,
534
700
  ],
535
701
  }));
536
702
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -707,26 +873,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
707
873
  if (!isSearchTasksArgs(args)) {
708
874
  throw new Error("Invalid arguments for todoist_search_tasks");
709
875
  }
710
- const params = {};
711
- if (args.projectId)
712
- params.projectId = args.projectId;
713
- const tasks = await todoistClient.getTasks(params);
714
- const allTasks = Array.isArray(tasks) ? tasks : tasks.results || [];
715
- const matchingTasks = allTasks.filter((task) => task.content.toLowerCase().includes(args.query.toLowerCase())).slice(0, args.limit || 10);
716
- if (matchingTasks.length === 0) {
876
+ // Prepare arguments for getTasksByFilter
877
+ // Prepend "search: " to the query for more robust keyword searching with Todoist API
878
+ const searchQuery = args.query.startsWith("search:") ? args.query : `search: ${args.query}`;
879
+ const filterArgs = { query: searchQuery };
880
+ if (args.limit)
881
+ filterArgs.limit = args.limit;
882
+ // Note: args.projectId is not directly used by getTasksByFilter unless incorporated into the query string.
883
+ // For example: `search: ${args.query} & #ProjectName` or `search: ${args.query} & ##ProjectID`
884
+ const tasksResponse = await todoistClient.getTasksByFilter(filterArgs);
885
+ const matchingTasksData = tasksResponse.results || [];
886
+ if (matchingTasksData.length === 0) {
717
887
  return {
718
888
  content: [{
719
889
  type: "text",
720
- text: `No tasks found matching "${args.query}"`
890
+ text: `No tasks found matching the filter query "${args.query}"`
721
891
  }],
722
892
  isError: false,
723
893
  };
724
894
  }
725
- const taskList = matchingTasks.map((task) => `ID: ${task.id}\n${formatTask(task)}`).join('\n\n');
895
+ // Asynchronously format tasks and fetch project names if necessary
896
+ const formattedTaskList = await Promise.all(matchingTasksData.map(async (task) => {
897
+ let taskDisplay = formatTask(task); // formatTask now includes Project ID
898
+ if (task.projectId) {
899
+ try {
900
+ const project = await todoistClient.getProject(task.projectId);
901
+ taskDisplay += `\n Project Name: ${project.name}`;
902
+ }
903
+ catch (projectError) {
904
+ // Silently ignore project fetch errors for search, or log them
905
+ // taskDisplay += `\n Project Name: (Error fetching project details)`;
906
+ console.error(`Error fetching project ${task.projectId} for search result: ${projectError.message}`);
907
+ }
908
+ }
909
+ return taskDisplay;
910
+ }));
911
+ const taskListString = formattedTaskList.join('\n\n');
912
+ const nextCursorMessage = tasksResponse.nextCursor ? `\n\nNext cursor for more results: ${tasksResponse.nextCursor}` : '';
726
913
  return {
727
914
  content: [{
728
915
  type: "text",
729
- text: `Found ${matchingTasks.length} task(s) matching "${args.query}":\n\n${taskList}`
916
+ text: `Found ${matchingTasksData.length} task(s) matching "${args.query}":\n\n${taskListString}${nextCursorMessage}`
730
917
  }],
731
918
  isError: false,
732
919
  };
@@ -741,16 +928,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
741
928
  params.cursor = args.cursor;
742
929
  if (args.limit)
743
930
  params.limit = args.limit;
744
- // Note: getProjects() may not accept parameters in this API version
745
- const projects = await todoistClient.getProjects();
746
- // Handle simple array response
747
- const projectList = Array.isArray(projects)
748
- ? projects.map(formatProject).join('\n\n')
749
- : 'No projects found';
931
+ const projectsResponse = await todoistClient.getProjects(params);
932
+ const projectList = projectsResponse.results?.map(formatProject).join('\n\n') || 'No projects found';
933
+ const nextCursor = projectsResponse.nextCursor ? `\n\nNext cursor: ${projectsResponse.nextCursor}` : '';
750
934
  return {
751
935
  content: [{
752
936
  type: "text",
753
- text: `Projects:\n${projectList}`
937
+ text: `Projects:\n${projectList}${nextCursor}`
754
938
  }],
755
939
  isError: false,
756
940
  };
@@ -890,6 +1074,121 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
890
1074
  isError: false,
891
1075
  };
892
1076
  }
1077
+ // Label operations
1078
+ if (name === "todoist_create_label") {
1079
+ if (!isCreateLabelArgs(args)) {
1080
+ return { content: [{ type: "text", text: "Invalid arguments for create_label" }], isError: true };
1081
+ }
1082
+ try {
1083
+ const label = await todoistClient.addLabel(args);
1084
+ return {
1085
+ content: [{ type: "text", text: `Label created:\n${formatLabel(label)}` }],
1086
+ isError: false
1087
+ };
1088
+ }
1089
+ catch (error) {
1090
+ return { content: [{ type: "text", text: `Error creating label: ${error.message}` }], isError: true };
1091
+ }
1092
+ }
1093
+ if (name === "todoist_get_label") {
1094
+ if (!isLabelIdArgs(args)) {
1095
+ return { content: [{ type: "text", text: "Invalid arguments for get_label" }], isError: true };
1096
+ }
1097
+ try {
1098
+ const label = await todoistClient.getLabel(args.labelId);
1099
+ return {
1100
+ content: [{ type: "text", text: `Label details:\n${formatLabel(label)}` }],
1101
+ isError: false
1102
+ };
1103
+ }
1104
+ catch (error) {
1105
+ return { content: [{ type: "text", text: `Error getting label: ${error.message}` }], isError: true };
1106
+ }
1107
+ }
1108
+ if (name === "todoist_get_labels") {
1109
+ if (!isGetLabelsArgs(args)) {
1110
+ return { content: [{ type: "text", text: "Invalid arguments for get_labels. This tool takes an optional cursor and limit." }], isError: true };
1111
+ }
1112
+ try {
1113
+ const params = {};
1114
+ if (args.cursor)
1115
+ params.cursor = args.cursor;
1116
+ if (args.limit)
1117
+ params.limit = args.limit;
1118
+ const labelsResponse = await todoistClient.getLabels(params);
1119
+ const labelList = labelsResponse.results?.map(formatLabel).join('\n\n') || 'No labels found';
1120
+ const nextCursor = labelsResponse.nextCursor ? `\n\nNext cursor for more labels: ${labelsResponse.nextCursor}` : '';
1121
+ return {
1122
+ content: [{
1123
+ type: "text",
1124
+ text: `Labels:\n${labelList}${nextCursor}`
1125
+ }],
1126
+ isError: false
1127
+ };
1128
+ }
1129
+ catch (error) {
1130
+ return { content: [{ type: "text", text: `Error getting labels: ${error.message}` }], isError: true };
1131
+ }
1132
+ }
1133
+ if (name === "todoist_update_label") {
1134
+ if (!isUpdateLabelArgs(args)) {
1135
+ return { content: [{ type: "text", text: "Invalid arguments for update_label" }], isError: true };
1136
+ }
1137
+ try {
1138
+ const { labelId, ...updateArgs } = args;
1139
+ const updatedLabel = await todoistClient.updateLabel(labelId, updateArgs);
1140
+ return {
1141
+ content: [{ type: "text", text: `Label updated:\n${formatLabel(updatedLabel)}` }],
1142
+ isError: false
1143
+ };
1144
+ }
1145
+ catch (error) {
1146
+ return { content: [{ type: "text", text: `Error updating label: ${error.message}` }], isError: true };
1147
+ }
1148
+ }
1149
+ if (name === "todoist_delete_label") {
1150
+ if (!isLabelIdArgs(args)) {
1151
+ return { content: [{ type: "text", text: "Invalid arguments for delete_label" }], isError: true };
1152
+ }
1153
+ try {
1154
+ await todoistClient.deleteLabel(args.labelId);
1155
+ return {
1156
+ content: [{ type: "text", text: `Label ${args.labelId} deleted.` }],
1157
+ isError: false
1158
+ };
1159
+ }
1160
+ catch (error) {
1161
+ return { content: [{ type: "text", text: `Error deleting label: ${error.message}` }], isError: true };
1162
+ }
1163
+ }
1164
+ // Move task operations
1165
+ if (name === "todoist_move_task") {
1166
+ if (!isMoveTaskArgs(args)) {
1167
+ return { content: [{ type: "text", text: "Invalid arguments for move_task. Provide taskId and exactly one of: projectId, sectionId, or parentId (must be a non-empty string)." }], isError: true };
1168
+ }
1169
+ try {
1170
+ const moveArgs = {};
1171
+ if (args.projectId)
1172
+ moveArgs.projectId = args.projectId;
1173
+ else if (args.sectionId)
1174
+ moveArgs.sectionId = args.sectionId;
1175
+ else if (args.parentId)
1176
+ moveArgs.parentId = args.parentId;
1177
+ // Use moveTasks from SDK v4+
1178
+ await todoistClient.moveTasks([args.taskId], moveArgs); // Cast to any for MoveTaskArgs as it expects RequireExactlyOne
1179
+ const movedTask = await todoistClient.getTask(args.taskId);
1180
+ return {
1181
+ content: [{
1182
+ type: "text",
1183
+ text: `Task ${args.taskId} moved successfully.\nNew details:\n${formatTask(movedTask)}`
1184
+ }],
1185
+ isError: false
1186
+ };
1187
+ }
1188
+ catch (error) {
1189
+ return { content: [{ type: "text", text: `Error moving task: ${error.message}` }], isError: true };
1190
+ }
1191
+ }
893
1192
  return {
894
1193
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
895
1194
  isError: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kydycode/todoist-mcp-server-ext",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Extended MCP server for Todoist API integration with enhanced features and improved compatibility",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "homepage": "https://github.com/kydycode/todoist-mcp-server-ext#readme",
39
39
  "dependencies": {
40
- "@doist/todoist-api-typescript": "^3.0.3",
40
+ "@doist/todoist-api-typescript": "^4.0.4",
41
41
  "@modelcontextprotocol/sdk": "0.5.0",
42
42
  "zod": "^3.22.0"
43
43
  },