ticktick-mcp 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.
package/dist/index.js CHANGED
@@ -329,10 +329,37 @@ server.tool("get_user", "Get the current authenticated user's information from T
329
329
  *
330
330
  * Returns all projects for the authenticated user.
331
331
  */
332
- server.tool("list_projects", "List all projects in the user's TickTick account.", {}, async () => {
332
+ server.tool("list_projects", "List all projects in the user's TickTick account with optional filtering. By default, only returns active (non-archived) projects.", {
333
+ includeArchived: z.boolean().optional().describe("Include archived/closed projects (default: false)"),
334
+ kind: z.enum(['TASK', 'NOTE']).optional().describe("Filter by project type"),
335
+ viewMode: z.enum(['list', 'kanban', 'timeline']).optional().describe("Filter by view mode"),
336
+ search: z.string().optional().describe("Text search in project name (case-insensitive)"),
337
+ limit: z.number().optional().describe("Maximum number of projects to return"),
338
+ }, async ({ includeArchived, kind, viewMode, search, limit }) => {
333
339
  try {
334
340
  const client = await getClient();
335
- const projects = await client.listProjects();
341
+ let projects = await client.listProjects();
342
+ // Filter out archived projects by default
343
+ if (!includeArchived) {
344
+ projects = projects.filter(p => !p.closed);
345
+ }
346
+ // Filter by kind
347
+ if (kind) {
348
+ projects = projects.filter(p => p.kind === kind);
349
+ }
350
+ // Filter by viewMode
351
+ if (viewMode) {
352
+ projects = projects.filter(p => p.viewMode === viewMode);
353
+ }
354
+ // Filter by search text
355
+ if (search) {
356
+ const searchLower = search.toLowerCase();
357
+ projects = projects.filter(p => p.name.toLowerCase().includes(searchLower));
358
+ }
359
+ // Apply limit
360
+ if (limit && limit > 0) {
361
+ projects = projects.slice(0, limit);
362
+ }
336
363
  return {
337
364
  content: [
338
365
  {
@@ -489,7 +516,7 @@ server.tool("create_project", "Create a new project in TickTick.", {
489
516
  const project = await client.createProject({
490
517
  name,
491
518
  color,
492
- viewMode,
519
+ viewMode: viewMode,
493
520
  });
494
521
  return {
495
522
  content: [
@@ -547,7 +574,7 @@ server.tool("update_project", "Update an existing project in TickTick.", {
547
574
  const project = await client.updateProject(projectId, {
548
575
  name,
549
576
  color,
550
- viewMode,
577
+ viewMode: viewMode,
551
578
  });
552
579
  return {
553
580
  content: [
@@ -632,20 +659,134 @@ server.tool("delete_project", "Delete a project from TickTick.", {
632
659
  * Returns all tasks in a specific project (without project metadata).
633
660
  * This is a convenience tool that wraps getProjectWithTasks but only returns tasks.
634
661
  */
635
- server.tool("list_tasks_in_project", "List all tasks in a specific project. Returns only the tasks array without project metadata.", {
662
+ server.tool("list_tasks_in_project", "List all tasks in a specific project with optional client-side filtering and sorting. Returns only the tasks array without project metadata.", {
636
663
  projectId: z.string().describe("The ID of the project to list tasks from"),
637
- }, async ({ projectId }) => {
664
+ status: z.enum(['active', 'completed', 'all']).optional().describe("Filter by task status (default: 'all')"),
665
+ priority: z.union([z.number(), z.array(z.number())]).optional().describe("Filter by priority: single value (0,1,3,5) or array [0,1,3,5]"),
666
+ dueBefore: z.string().optional().describe("ISO date - include only tasks due before this date"),
667
+ dueAfter: z.string().optional().describe("ISO date - include only tasks due after this date"),
668
+ startBefore: z.string().optional().describe("ISO date - include only tasks starting before this date"),
669
+ startAfter: z.string().optional().describe("ISO date - include only tasks starting after this date"),
670
+ hasSubtasks: z.boolean().optional().describe("Filter by presence of subtasks: true=with subtasks, false=without"),
671
+ tags: z.array(z.string()).optional().describe("Filter by tags - include tasks with any of these tags"),
672
+ search: z.string().optional().describe("Text search - filter tasks whose title or content contains this text (case-insensitive)"),
673
+ sortBy: z.enum(['dueDate', 'priority', 'title', 'createdTime', 'modifiedTime', 'sortOrder']).optional().describe("Field to sort by"),
674
+ sortDirection: z.enum(['asc', 'desc']).optional().describe("Sort direction (default: 'asc')"),
675
+ limit: z.number().optional().describe("Maximum number of tasks to return"),
676
+ }, async ({ projectId, status, priority, dueBefore, dueAfter, startBefore, startAfter, hasSubtasks, tags, search, sortBy, sortDirection, limit }) => {
638
677
  try {
639
678
  const client = await getClient();
640
679
  const data = await client.getProjectWithTasks(projectId);
680
+ // Apply filters
681
+ let filteredTasks = data.tasks;
682
+ // Filter by status
683
+ if (status && status !== 'all') {
684
+ filteredTasks = filteredTasks.filter(t => {
685
+ if (status === 'active')
686
+ return t.status === 0;
687
+ if (status === 'completed')
688
+ return t.status === 2;
689
+ return true;
690
+ });
691
+ }
692
+ // Filter by priority
693
+ if (priority !== undefined) {
694
+ const priorities = Array.isArray(priority) ? priority : [priority];
695
+ filteredTasks = filteredTasks.filter(t => priorities.includes(t.priority));
696
+ }
697
+ // Filter by due date
698
+ if (dueBefore) {
699
+ const beforeDate = new Date(dueBefore);
700
+ filteredTasks = filteredTasks.filter(t => t.dueDate && new Date(t.dueDate) < beforeDate);
701
+ }
702
+ if (dueAfter) {
703
+ const afterDate = new Date(dueAfter);
704
+ filteredTasks = filteredTasks.filter(t => t.dueDate && new Date(t.dueDate) > afterDate);
705
+ }
706
+ // Filter by start date
707
+ if (startBefore) {
708
+ const beforeDate = new Date(startBefore);
709
+ filteredTasks = filteredTasks.filter(t => t.startDate && new Date(t.startDate) < beforeDate);
710
+ }
711
+ if (startAfter) {
712
+ const afterDate = new Date(startAfter);
713
+ filteredTasks = filteredTasks.filter(t => t.startDate && new Date(t.startDate) > afterDate);
714
+ }
715
+ // Filter by subtasks
716
+ if (hasSubtasks !== undefined) {
717
+ filteredTasks = filteredTasks.filter(t => {
718
+ const hasItems = t.items && t.items.length > 0;
719
+ return hasSubtasks ? hasItems : !hasItems;
720
+ });
721
+ }
722
+ // Filter by tags
723
+ if (tags && tags.length > 0) {
724
+ filteredTasks = filteredTasks.filter(t => {
725
+ return t.tags && t.tags.some(tag => tags.includes(tag));
726
+ });
727
+ }
728
+ // Filter by search text
729
+ if (search) {
730
+ const searchLower = search.toLowerCase();
731
+ filteredTasks = filteredTasks.filter(t => {
732
+ return t.title.toLowerCase().includes(searchLower) ||
733
+ (t.content && t.content.toLowerCase().includes(searchLower));
734
+ });
735
+ }
736
+ // Apply sorting
737
+ if (sortBy) {
738
+ const direction = sortDirection === 'desc' ? -1 : 1;
739
+ filteredTasks.sort((a, b) => {
740
+ let aVal;
741
+ let bVal;
742
+ switch (sortBy) {
743
+ case 'dueDate':
744
+ aVal = a.dueDate ? new Date(a.dueDate).getTime() : Infinity;
745
+ bVal = b.dueDate ? new Date(b.dueDate).getTime() : Infinity;
746
+ break;
747
+ case 'priority':
748
+ aVal = a.priority;
749
+ bVal = b.priority;
750
+ break;
751
+ case 'title':
752
+ aVal = a.title.toLowerCase();
753
+ bVal = b.title.toLowerCase();
754
+ break;
755
+ case 'createdTime':
756
+ aVal = new Date(a.createdTime).getTime();
757
+ bVal = new Date(b.createdTime).getTime();
758
+ break;
759
+ case 'modifiedTime':
760
+ aVal = new Date(a.modifiedTime).getTime();
761
+ bVal = new Date(b.modifiedTime).getTime();
762
+ break;
763
+ case 'sortOrder':
764
+ aVal = a.sortOrder;
765
+ bVal = b.sortOrder;
766
+ break;
767
+ default:
768
+ return 0;
769
+ }
770
+ if (aVal < bVal)
771
+ return -1 * direction;
772
+ if (aVal > bVal)
773
+ return 1 * direction;
774
+ return 0;
775
+ });
776
+ }
777
+ // Apply limit
778
+ if (limit && limit > 0) {
779
+ filteredTasks = filteredTasks.slice(0, limit);
780
+ }
641
781
  return {
642
782
  content: [
643
783
  {
644
784
  type: "text",
645
785
  text: JSON.stringify({
646
786
  success: true,
647
- count: data.tasks.length,
648
- tasks: data.tasks.map((t) => ({
787
+ total: data.tasks.length,
788
+ count: filteredTasks.length,
789
+ tasks: filteredTasks.map((t) => ({
649
790
  id: t.id,
650
791
  title: t.title,
651
792
  content: t.content,
@@ -653,7 +794,7 @@ server.tool("list_tasks_in_project", "List all tasks in a specific project. Retu
653
794
  status: t.status,
654
795
  dueDate: t.dueDate,
655
796
  startDate: t.startDate,
656
- allDay: t.allDay,
797
+ isAllDay: t.isAllDay,
657
798
  tags: t.tags,
658
799
  items: t.items?.map((item) => ({
659
800
  id: item.id,
@@ -705,7 +846,7 @@ server.tool("create_task", "Create a new task in TickTick.", {
705
846
  .optional()
706
847
  .describe("Priority: 0=None, 1=Low, 3=Medium, 5=High"),
707
848
  tags: z.array(z.string()).optional().describe("Array of tag names"),
708
- allDay: z
849
+ isAllDay: z
709
850
  .boolean()
710
851
  .optional()
711
852
  .describe("Whether this is an all-day task (no specific time)"),
@@ -737,7 +878,7 @@ server.tool("create_task", "Create a new task in TickTick.", {
737
878
  }))
738
879
  .optional()
739
880
  .describe("Array of subtask/checklist items"),
740
- }, async ({ title, projectId, content, dueDate, priority, tags, allDay, startDate, timeZone, reminders, repeat, items }) => {
881
+ }, async ({ title, projectId, content, dueDate, priority, tags, isAllDay, startDate, timeZone, reminders, repeat, items }) => {
741
882
  try {
742
883
  const client = await getClient();
743
884
  const task = await client.createTask({
@@ -746,11 +887,11 @@ server.tool("create_task", "Create a new task in TickTick.", {
746
887
  content,
747
888
  dueDate,
748
889
  priority,
749
- allDay,
890
+ isAllDay,
750
891
  startDate,
751
892
  timeZone,
752
893
  reminders,
753
- repeat,
894
+ repeatFlag: repeat,
754
895
  items,
755
896
  });
756
897
  // If tags are provided, update the task with tags
@@ -773,10 +914,10 @@ server.tool("create_task", "Create a new task in TickTick.", {
773
914
  priority: finalTask.priority,
774
915
  dueDate: finalTask.dueDate,
775
916
  startDate: finalTask.startDate,
776
- allDay: finalTask.allDay,
917
+ isAllDay: finalTask.isAllDay,
777
918
  timeZone: finalTask.timeZone,
778
919
  reminders: finalTask.reminders,
779
- repeat: finalTask.repeat,
920
+ repeat: finalTask.repeatFlag,
780
921
  status: finalTask.status,
781
922
  tags: finalTask.tags,
782
923
  items: finalTask.items?.map((item) => ({
@@ -831,7 +972,7 @@ server.tool("update_task", "Update an existing task in TickTick. Supports all ta
831
972
  .string()
832
973
  .optional()
833
974
  .describe("Move task to a different project by specifying the target project ID"),
834
- allDay: z
975
+ isAllDay: z
835
976
  .boolean()
836
977
  .optional()
837
978
  .describe("Whether this is an all-day task (no specific time)"),
@@ -866,7 +1007,7 @@ server.tool("update_task", "Update an existing task in TickTick. Supports all ta
866
1007
  }))
867
1008
  .optional()
868
1009
  .describe("Array of subtask/checklist items. Note: This replaces existing items when provided."),
869
- }, async ({ taskId, title, content, dueDate, priority, tags, projectId, allDay, startDate, timeZone, reminders, repeat, items }) => {
1010
+ }, async ({ taskId, title, content, dueDate, priority, tags, projectId, isAllDay, startDate, timeZone, reminders, repeat, items }) => {
870
1011
  try {
871
1012
  const client = await getClient();
872
1013
  const task = await client.updateTask(taskId, {
@@ -876,11 +1017,11 @@ server.tool("update_task", "Update an existing task in TickTick. Supports all ta
876
1017
  priority,
877
1018
  tags,
878
1019
  projectId,
879
- allDay,
1020
+ isAllDay,
880
1021
  startDate,
881
1022
  timeZone,
882
1023
  reminders: reminders === null ? undefined : reminders,
883
- repeat: repeat === null ? "" : repeat,
1024
+ repeatFlag: repeat === null ? "" : repeat,
884
1025
  items,
885
1026
  });
886
1027
  return {
@@ -898,10 +1039,10 @@ server.tool("update_task", "Update an existing task in TickTick. Supports all ta
898
1039
  priority: task.priority,
899
1040
  dueDate: task.dueDate,
900
1041
  startDate: task.startDate,
901
- allDay: task.allDay,
1042
+ isAllDay: task.isAllDay,
902
1043
  timeZone: task.timeZone,
903
1044
  reminders: task.reminders,
904
- repeat: task.repeat,
1045
+ repeat: task.repeatFlag,
905
1046
  status: task.status,
906
1047
  tags: task.tags,
907
1048
  items: task.items?.map((item) => ({
@@ -1038,7 +1179,7 @@ server.tool("get_task", "Get a specific task from TickTick.", {
1038
1179
  status: task.status,
1039
1180
  dueDate: task.dueDate,
1040
1181
  startDate: task.startDate,
1041
- allDay: task.allDay,
1182
+ isAllDay: task.isAllDay,
1042
1183
  tags: task.tags,
1043
1184
  items: task.items.map((item) => ({
1044
1185
  id: item.id,
@@ -1137,6 +1278,351 @@ server.tool("batch_create_tasks", "Create multiple tasks at once in TickTick. Mo
1137
1278
  };
1138
1279
  }
1139
1280
  });
1281
+ /**
1282
+ * Tool: Get tasks due soon
1283
+ *
1284
+ * Returns tasks due within a specified number of days across all projects.
1285
+ */
1286
+ server.tool("get_tasks_due_soon", "Get tasks that are due soon (within the next N days). Useful for finding urgent work across all projects.", {
1287
+ days: z.number().optional().describe("Number of days to look ahead (default: 7)"),
1288
+ includeOverdue: z.boolean().optional().describe("Include past-due tasks (default: true)"),
1289
+ projectId: z.string().optional().describe("Optional: limit to a specific project"),
1290
+ status: z.enum(['active', 'all']).optional().describe("Task status filter (default: 'active')"),
1291
+ }, async ({ days = 7, includeOverdue = true, projectId, status = 'active' }) => {
1292
+ try {
1293
+ const client = await getClient();
1294
+ // Calculate date range
1295
+ const now = new Date();
1296
+ const futureDate = new Date();
1297
+ futureDate.setDate(now.getDate() + days);
1298
+ let allTasks = [];
1299
+ if (projectId) {
1300
+ // Fetch tasks from specific project
1301
+ const data = await client.getProjectWithTasks(projectId);
1302
+ allTasks = data.tasks.map(t => ({
1303
+ ...t,
1304
+ projectName: data.project.name,
1305
+ }));
1306
+ }
1307
+ else {
1308
+ // Fetch all projects and their tasks
1309
+ const projects = await client.listProjects();
1310
+ for (const project of projects) {
1311
+ if (!project.closed) { // Skip archived projects
1312
+ try {
1313
+ const data = await client.getProjectWithTasks(project.id);
1314
+ const tasksWithProject = data.tasks.map(t => ({
1315
+ ...t,
1316
+ projectName: project.name,
1317
+ }));
1318
+ allTasks = allTasks.concat(tasksWithProject);
1319
+ }
1320
+ catch {
1321
+ // Skip projects that can't be fetched
1322
+ continue;
1323
+ }
1324
+ }
1325
+ }
1326
+ }
1327
+ // Filter tasks
1328
+ let filteredTasks = allTasks;
1329
+ // Filter by status
1330
+ if (status === 'active') {
1331
+ filteredTasks = filteredTasks.filter(t => t.status === 0);
1332
+ }
1333
+ // Filter by due date
1334
+ filteredTasks = filteredTasks.filter(t => {
1335
+ if (!t.dueDate)
1336
+ return false;
1337
+ const dueDate = new Date(t.dueDate);
1338
+ const isOverdue = dueDate < now;
1339
+ const isDueSoon = dueDate >= now && dueDate <= futureDate;
1340
+ if (includeOverdue && isOverdue)
1341
+ return true;
1342
+ if (isDueSoon)
1343
+ return true;
1344
+ return false;
1345
+ });
1346
+ // Sort by due date ascending (earliest first)
1347
+ filteredTasks.sort((a, b) => {
1348
+ const aDate = new Date(a.dueDate).getTime();
1349
+ const bDate = new Date(b.dueDate).getTime();
1350
+ return aDate - bDate;
1351
+ });
1352
+ return {
1353
+ content: [
1354
+ {
1355
+ type: "text",
1356
+ text: JSON.stringify({
1357
+ success: true,
1358
+ count: filteredTasks.length,
1359
+ daysAhead: days,
1360
+ tasks: filteredTasks.map(t => ({
1361
+ id: t.id,
1362
+ title: t.title,
1363
+ projectId: t.projectId,
1364
+ projectName: t.projectName,
1365
+ dueDate: t.dueDate,
1366
+ priority: t.priority,
1367
+ status: t.status,
1368
+ content: t.content,
1369
+ tags: t.tags,
1370
+ })),
1371
+ }, null, 2),
1372
+ },
1373
+ ],
1374
+ };
1375
+ }
1376
+ catch (error) {
1377
+ const errorMessage = error instanceof Error ? error.message : String(error);
1378
+ return {
1379
+ content: [
1380
+ {
1381
+ type: "text",
1382
+ text: JSON.stringify({
1383
+ success: false,
1384
+ error: errorMessage,
1385
+ }, null, 2),
1386
+ },
1387
+ ],
1388
+ isError: true,
1389
+ };
1390
+ }
1391
+ });
1392
+ /**
1393
+ * Tool: Search tasks
1394
+ *
1395
+ * Search for tasks by keyword across all projects.
1396
+ */
1397
+ server.tool("search_tasks", "Search for tasks by keyword across all projects. Useful for finding tasks by text content.", {
1398
+ query: z.string().describe("Search term (required)"),
1399
+ searchIn: z.array(z.enum(['title', 'content', 'desc'])).optional().describe("Fields to search in (default: ['title', 'content'])"),
1400
+ projectIds: z.array(z.string()).optional().describe("Optional: limit to specific project IDs"),
1401
+ status: z.enum(['active', 'completed', 'all']).optional().describe("Task status filter (default: 'active')"),
1402
+ caseSensitive: z.boolean().optional().describe("Case-sensitive search (default: false)"),
1403
+ limit: z.number().optional().describe("Maximum results to return (default: 20)"),
1404
+ }, async ({ query, searchIn = ['title', 'content'], projectIds, status = 'active', caseSensitive = false, limit = 20 }) => {
1405
+ try {
1406
+ const client = await getClient();
1407
+ const searchQuery = caseSensitive ? query : query.toLowerCase();
1408
+ let allTasks = [];
1409
+ if (projectIds && projectIds.length > 0) {
1410
+ // Search in specific projects
1411
+ for (const projectId of projectIds) {
1412
+ try {
1413
+ const data = await client.getProjectWithTasks(projectId);
1414
+ const tasksWithProject = data.tasks.map(t => ({
1415
+ ...t,
1416
+ projectName: data.project.name,
1417
+ }));
1418
+ allTasks = allTasks.concat(tasksWithProject);
1419
+ }
1420
+ catch {
1421
+ continue;
1422
+ }
1423
+ }
1424
+ }
1425
+ else {
1426
+ // Search all projects
1427
+ const projects = await client.listProjects();
1428
+ for (const project of projects) {
1429
+ if (!project.closed) {
1430
+ try {
1431
+ const data = await client.getProjectWithTasks(project.id);
1432
+ const tasksWithProject = data.tasks.map(t => ({
1433
+ ...t,
1434
+ projectName: project.name,
1435
+ }));
1436
+ allTasks = allTasks.concat(tasksWithProject);
1437
+ }
1438
+ catch {
1439
+ continue;
1440
+ }
1441
+ }
1442
+ }
1443
+ }
1444
+ // Filter by status
1445
+ let filteredTasks = allTasks;
1446
+ if (status === 'active') {
1447
+ filteredTasks = filteredTasks.filter(t => t.status === 0);
1448
+ }
1449
+ else if (status === 'completed') {
1450
+ filteredTasks = filteredTasks.filter(t => t.status === 2);
1451
+ }
1452
+ // Search and collect matches
1453
+ const matches = [];
1454
+ for (const task of filteredTasks) {
1455
+ const matchedIn = [];
1456
+ for (const field of searchIn) {
1457
+ let fieldValue = '';
1458
+ if (field === 'title')
1459
+ fieldValue = task.title || '';
1460
+ else if (field === 'content')
1461
+ fieldValue = task.content || '';
1462
+ else if (field === 'desc')
1463
+ fieldValue = task.desc || '';
1464
+ const searchText = caseSensitive ? fieldValue : fieldValue.toLowerCase();
1465
+ if (searchText.includes(searchQuery)) {
1466
+ matchedIn.push(field);
1467
+ }
1468
+ }
1469
+ if (matchedIn.length > 0) {
1470
+ matches.push({
1471
+ ...task,
1472
+ matchedIn,
1473
+ });
1474
+ }
1475
+ if (matches.length >= limit)
1476
+ break;
1477
+ }
1478
+ return {
1479
+ content: [
1480
+ {
1481
+ type: "text",
1482
+ text: JSON.stringify({
1483
+ success: true,
1484
+ query: query,
1485
+ count: matches.length,
1486
+ tasks: matches.map(t => ({
1487
+ id: t.id,
1488
+ title: t.title,
1489
+ projectId: t.projectId,
1490
+ projectName: t.projectName,
1491
+ content: t.content,
1492
+ dueDate: t.dueDate,
1493
+ priority: t.priority,
1494
+ status: t.status,
1495
+ tags: t.tags,
1496
+ matchedIn: t.matchedIn,
1497
+ })),
1498
+ }, null, 2),
1499
+ },
1500
+ ],
1501
+ };
1502
+ }
1503
+ catch (error) {
1504
+ const errorMessage = error instanceof Error ? error.message : String(error);
1505
+ return {
1506
+ content: [
1507
+ {
1508
+ type: "text",
1509
+ text: JSON.stringify({
1510
+ success: false,
1511
+ error: errorMessage,
1512
+ }, null, 2),
1513
+ },
1514
+ ],
1515
+ isError: true,
1516
+ };
1517
+ }
1518
+ });
1519
+ /**
1520
+ * Tool: Get high priority tasks
1521
+ *
1522
+ * Returns high priority tasks across all projects.
1523
+ */
1524
+ server.tool("get_high_priority_tasks", "Get high priority tasks across all projects. Useful for finding the most important work.", {
1525
+ minPriority: z.number().optional().describe("Minimum priority level: 0=None, 1=Low, 3=Medium, 5=High (default: 3)"),
1526
+ projectId: z.string().optional().describe("Optional: limit to a specific project"),
1527
+ includeCompleted: z.boolean().optional().describe("Include completed tasks (default: false)"),
1528
+ limit: z.number().optional().describe("Maximum results to return (default: 10)"),
1529
+ }, async ({ minPriority = 3, projectId, includeCompleted = false, limit = 10 }) => {
1530
+ try {
1531
+ const client = await getClient();
1532
+ let allTasks = [];
1533
+ if (projectId) {
1534
+ // Fetch tasks from specific project
1535
+ const data = await client.getProjectWithTasks(projectId);
1536
+ allTasks = data.tasks.map(t => ({
1537
+ ...t,
1538
+ projectName: data.project.name,
1539
+ }));
1540
+ }
1541
+ else {
1542
+ // Fetch all projects and their tasks
1543
+ const projects = await client.listProjects();
1544
+ for (const project of projects) {
1545
+ if (!project.closed) {
1546
+ try {
1547
+ const data = await client.getProjectWithTasks(project.id);
1548
+ const tasksWithProject = data.tasks.map(t => ({
1549
+ ...t,
1550
+ projectName: project.name,
1551
+ }));
1552
+ allTasks = allTasks.concat(tasksWithProject);
1553
+ }
1554
+ catch {
1555
+ continue;
1556
+ }
1557
+ }
1558
+ }
1559
+ }
1560
+ // Filter tasks
1561
+ let filteredTasks = allTasks;
1562
+ // Filter by completion status
1563
+ if (!includeCompleted) {
1564
+ filteredTasks = filteredTasks.filter(t => t.status === 0);
1565
+ }
1566
+ // Filter by priority
1567
+ filteredTasks = filteredTasks.filter(t => t.priority >= minPriority);
1568
+ // Sort by priority desc (high first), then by dueDate asc (earliest first)
1569
+ filteredTasks.sort((a, b) => {
1570
+ // First sort by priority (descending)
1571
+ if (a.priority !== b.priority) {
1572
+ return b.priority - a.priority;
1573
+ }
1574
+ // Then by due date (ascending, nulls last)
1575
+ if (a.dueDate && b.dueDate) {
1576
+ return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
1577
+ }
1578
+ if (a.dueDate)
1579
+ return -1;
1580
+ if (b.dueDate)
1581
+ return 1;
1582
+ return 0;
1583
+ });
1584
+ // Apply limit
1585
+ filteredTasks = filteredTasks.slice(0, limit);
1586
+ return {
1587
+ content: [
1588
+ {
1589
+ type: "text",
1590
+ text: JSON.stringify({
1591
+ success: true,
1592
+ count: filteredTasks.length,
1593
+ minPriority,
1594
+ tasks: filteredTasks.map(t => ({
1595
+ id: t.id,
1596
+ title: t.title,
1597
+ projectId: t.projectId,
1598
+ projectName: t.projectName,
1599
+ dueDate: t.dueDate,
1600
+ priority: t.priority,
1601
+ status: t.status,
1602
+ content: t.content,
1603
+ tags: t.tags,
1604
+ })),
1605
+ }, null, 2),
1606
+ },
1607
+ ],
1608
+ };
1609
+ }
1610
+ catch (error) {
1611
+ const errorMessage = error instanceof Error ? error.message : String(error);
1612
+ return {
1613
+ content: [
1614
+ {
1615
+ type: "text",
1616
+ text: JSON.stringify({
1617
+ success: false,
1618
+ error: errorMessage,
1619
+ }, null, 2),
1620
+ },
1621
+ ],
1622
+ isError: true,
1623
+ };
1624
+ }
1625
+ });
1140
1626
  // =============================================================================
1141
1627
  // Main Entry Point
1142
1628
  // =============================================================================