motionmcp 2.2.4 → 2.4.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 (46) hide show
  1. package/README.md +121 -164
  2. package/dist/handlers/ProjectHandler.js +4 -4
  3. package/dist/handlers/ProjectHandler.js.map +1 -1
  4. package/dist/handlers/RecurringTaskHandler.js +2 -2
  5. package/dist/handlers/RecurringTaskHandler.js.map +1 -1
  6. package/dist/handlers/SearchHandler.d.ts.map +1 -1
  7. package/dist/handlers/SearchHandler.js +20 -4
  8. package/dist/handlers/SearchHandler.js.map +1 -1
  9. package/dist/handlers/TaskHandler.d.ts.map +1 -1
  10. package/dist/handlers/TaskHandler.js +31 -5
  11. package/dist/handlers/TaskHandler.js.map +1 -1
  12. package/dist/mcp-server-old.d.ts +29 -0
  13. package/dist/mcp-server-old.d.ts.map +1 -0
  14. package/dist/mcp-server-old.js +1304 -0
  15. package/dist/mcp-server-old.js.map +1 -0
  16. package/dist/mcp-server.js +0 -0
  17. package/dist/services/motionApi.d.ts +11 -9
  18. package/dist/services/motionApi.d.ts.map +1 -1
  19. package/dist/services/motionApi.js +182 -120
  20. package/dist/services/motionApi.js.map +1 -1
  21. package/dist/tools/ToolDefinitions.d.ts.map +1 -1
  22. package/dist/tools/ToolDefinitions.js +9 -2
  23. package/dist/tools/ToolDefinitions.js.map +1 -1
  24. package/dist/types/mcp-tool-args.d.ts +2 -1
  25. package/dist/types/mcp-tool-args.d.ts.map +1 -1
  26. package/dist/types/mcp.d.ts +10 -0
  27. package/dist/types/mcp.d.ts.map +1 -1
  28. package/dist/utils/index.d.ts +1 -0
  29. package/dist/utils/index.d.ts.map +1 -1
  30. package/dist/utils/jsonSchemaToZod.d.ts +33 -0
  31. package/dist/utils/jsonSchemaToZod.d.ts.map +1 -0
  32. package/dist/utils/jsonSchemaToZod.js +110 -0
  33. package/dist/utils/jsonSchemaToZod.js.map +1 -0
  34. package/dist/utils/paginationNew.d.ts +2 -0
  35. package/dist/utils/paginationNew.d.ts.map +1 -1
  36. package/dist/utils/paginationNew.js +10 -1
  37. package/dist/utils/paginationNew.js.map +1 -1
  38. package/dist/utils/responseFormatters.d.ts +9 -1
  39. package/dist/utils/responseFormatters.d.ts.map +1 -1
  40. package/dist/utils/responseFormatters.js +33 -7
  41. package/dist/utils/responseFormatters.js.map +1 -1
  42. package/dist/worker.d.ts +17 -0
  43. package/dist/worker.d.ts.map +1 -0
  44. package/dist/worker.js +61 -0
  45. package/dist/worker.js.map +1 -0
  46. package/package.json +6 -2
@@ -83,16 +83,16 @@ class MotionApiService {
83
83
  throw error;
84
84
  }
85
85
  }
86
- constructor() {
87
- const apiKey = process.env.MOTION_API_KEY;
88
- if (!apiKey) {
86
+ constructor(apiKey) {
87
+ const resolvedApiKey = apiKey || process.env.MOTION_API_KEY;
88
+ if (!resolvedApiKey) {
89
89
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Motion API key not found in environment variables', {
90
90
  component: 'MotionApiService',
91
91
  method: 'constructor'
92
92
  });
93
93
  throw new Error('MOTION_API_KEY environment variable is required');
94
94
  }
95
- this.apiKey = apiKey;
95
+ this.apiKey = resolvedApiKey;
96
96
  this.baseUrl = 'https://api.usemotion.com/v1';
97
97
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Initializing Motion API service', {
98
98
  component: 'MotionApiService',
@@ -230,70 +230,77 @@ class MotionApiService {
230
230
  throw new Error('limit must be a non-negative integer');
231
231
  }
232
232
  const cacheKey = `projects:workspace:${workspaceId}`;
233
- return this.projectCache.withCache(cacheKey, async () => {
233
+ // Check cache - return items only (no stale truncation info)
234
+ const cachedItems = this.projectCache.get(cacheKey);
235
+ if (cachedItems !== null) {
236
+ return { items: cachedItems };
237
+ }
238
+ try {
239
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from Motion API', {
240
+ method: 'getProjects',
241
+ workspaceId,
242
+ maxPages,
243
+ limit
244
+ });
245
+ // Create a fetch function for potential pagination
246
+ const fetchPage = async (cursor) => {
247
+ const params = new URLSearchParams();
248
+ params.append('workspaceId', workspaceId);
249
+ if (cursor) {
250
+ params.append('cursor', cursor);
251
+ }
252
+ const queryString = params.toString();
253
+ const url = `/projects?${queryString}`;
254
+ return this.requestWithRetry(() => this.client.get(url));
255
+ };
234
256
  try {
235
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from Motion API', {
236
- method: 'getProjects',
237
- workspaceId,
257
+ // Attempt pagination-aware fetch with new response wrapper
258
+ const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'projects', {
238
259
  maxPages,
239
- limit
260
+ logProgress: false,
261
+ ...(limit ? { maxItems: limit } : {})
240
262
  });
241
- // Create a fetch function for potential pagination
242
- const fetchPage = async (cursor) => {
243
- const params = new URLSearchParams();
244
- params.append('workspaceId', workspaceId);
245
- if (cursor) {
246
- params.append('cursor', cursor);
247
- }
248
- const queryString = params.toString();
249
- const url = `/projects?${queryString}`;
250
- return this.requestWithRetry(() => this.client.get(url));
251
- };
252
- try {
253
- // Attempt pagination-aware fetch with new response wrapper
254
- const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'projects', {
255
- maxPages,
256
- logProgress: false,
257
- ...(limit ? { maxItems: limit } : {})
258
- });
259
- if (paginatedResult.totalFetched > 0) {
260
- let projects = paginatedResult.items;
261
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully with pagination', {
262
- method: 'getProjects',
263
- totalCount: projects.length,
264
- hasMore: paginatedResult.hasMore,
265
- workspaceId
266
- });
267
- return projects;
268
- }
269
- }
270
- catch (paginationError) {
271
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
263
+ if (paginatedResult.totalFetched > 0) {
264
+ let projects = paginatedResult.items;
265
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully with pagination', {
272
266
  method: 'getProjects',
273
- error: paginationError instanceof Error ? paginationError.message : String(paginationError)
267
+ totalCount: projects.length,
268
+ hasMore: paginatedResult.hasMore,
269
+ workspaceId
274
270
  });
271
+ // Cache only items, not truncation metadata
272
+ this.projectCache.set(cacheKey, projects);
273
+ return { items: projects, truncation: paginatedResult.truncation };
275
274
  }
276
- // Use new response wrapper for single page fallback
277
- const response = await fetchPage();
278
- const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'projects');
279
- let projects = unwrapped.data;
280
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully (single page)', {
281
- method: 'getProjects',
282
- count: projects.length,
283
- workspaceId
284
- });
285
- return projects;
286
275
  }
287
- catch (error) {
288
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects', {
276
+ catch (paginationError) {
277
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
289
278
  method: 'getProjects',
290
- error: getErrorMessage(error),
291
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
292
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
279
+ error: paginationError instanceof Error ? paginationError.message : String(paginationError)
293
280
  });
294
- throw this.formatApiError(error, 'fetch', 'project');
295
281
  }
296
- });
282
+ // Use new response wrapper for single page fallback
283
+ const response = await fetchPage();
284
+ const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'projects');
285
+ let projects = unwrapped.data;
286
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully (single page)', {
287
+ method: 'getProjects',
288
+ count: projects.length,
289
+ workspaceId
290
+ });
291
+ // Cache only items
292
+ this.projectCache.set(cacheKey, projects);
293
+ return { items: projects };
294
+ }
295
+ catch (error) {
296
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects', {
297
+ method: 'getProjects',
298
+ error: getErrorMessage(error),
299
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
300
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
301
+ });
302
+ throw this.formatApiError(error, 'fetch', 'project');
303
+ }
297
304
  }
298
305
  async getAllProjects() {
299
306
  try {
@@ -302,10 +309,14 @@ class MotionApiService {
302
309
  });
303
310
  const allWorkspaces = await this.getWorkspaces();
304
311
  const allProjects = [];
312
+ let aggregateTruncation;
305
313
  for (const workspace of allWorkspaces) {
306
314
  try {
307
- const workspaceProjects = await this.getProjects(workspace.id);
308
- allProjects.push(...workspaceProjects);
315
+ const { items, truncation } = await this.getProjects(workspace.id);
316
+ allProjects.push(...items);
317
+ if (truncation?.wasTruncated && !aggregateTruncation) {
318
+ aggregateTruncation = { ...truncation, returnedCount: allProjects.length };
319
+ }
309
320
  }
310
321
  catch (workspaceError) {
311
322
  // Log error but continue with other workspaces
@@ -322,7 +333,10 @@ class MotionApiService {
322
333
  totalProjects: allProjects.length,
323
334
  workspaceCount: allWorkspaces.length
324
335
  });
325
- return allProjects;
336
+ if (aggregateTruncation) {
337
+ aggregateTruncation.returnedCount = allProjects.length;
338
+ }
339
+ return { items: allProjects, truncation: aggregateTruncation };
326
340
  }
327
341
  catch (error) {
328
342
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects from all workspaces', {
@@ -457,7 +471,7 @@ class MotionApiService {
457
471
  // TASK API METHODS
458
472
  // ========================================
459
473
  async getTasks(options) {
460
- const { workspaceId, projectId, status, assigneeId, priority, dueDate, labels, limit, maxPages = 5 } = options;
474
+ const { workspaceId, projectId, status, includeAllStatuses, assigneeId, priority, dueDate, labels, limit, maxPages = 5 } = options;
461
475
  // Validate limit parameter if provided
462
476
  if (limit !== undefined && (limit < 0 || !Number.isInteger(limit))) {
463
477
  throw new Error('limit must be a non-negative integer');
@@ -468,6 +482,7 @@ class MotionApiService {
468
482
  workspaceId,
469
483
  projectId,
470
484
  status,
485
+ includeAllStatuses,
471
486
  assigneeId,
472
487
  priority,
473
488
  dueDate,
@@ -482,7 +497,22 @@ class MotionApiService {
482
497
  params.append('projectId', projectId);
483
498
  }
484
499
  if (status) {
485
- params.append('status', status);
500
+ if (Array.isArray(status)) {
501
+ // Deduplicate and skip empty strings before appending
502
+ // Note: Motion API supports repeated status= params for multi-value filtering
503
+ const uniqueStatuses = Array.from(new Set(status));
504
+ for (const s of uniqueStatuses) {
505
+ if (s) {
506
+ params.append('status', s);
507
+ }
508
+ }
509
+ }
510
+ else {
511
+ params.append('status', status);
512
+ }
513
+ }
514
+ if (includeAllStatuses) {
515
+ params.append('includeAllStatuses', 'true');
486
516
  }
487
517
  if (assigneeId) {
488
518
  params.append('assigneeId', assigneeId);
@@ -525,7 +555,7 @@ class MotionApiService {
525
555
  projectId,
526
556
  limitApplied: limit
527
557
  });
528
- return paginatedResult.items;
558
+ return { items: paginatedResult.items, truncation: paginatedResult.truncation };
529
559
  }
530
560
  }
531
561
  catch (paginationError) {
@@ -550,7 +580,7 @@ class MotionApiService {
550
580
  projectId,
551
581
  limitApplied: limit
552
582
  });
553
- return tasks;
583
+ return { items: tasks };
554
584
  }
555
585
  catch (error) {
556
586
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch tasks', {
@@ -1010,7 +1040,7 @@ class MotionApiService {
1010
1040
  workspaceId
1011
1041
  });
1012
1042
  // First, search in the specified workspace
1013
- const projects = await this.getProjects(workspaceId);
1043
+ const { items: projects } = await this.getProjects(workspaceId);
1014
1044
  const project = projects.find(p => p.name === projectName);
1015
1045
  if (project) {
1016
1046
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project found by name in specified workspace', {
@@ -1037,7 +1067,7 @@ class MotionApiService {
1037
1067
  searchingWorkspaceId: workspace.id,
1038
1068
  searchingWorkspaceName: workspace.name
1039
1069
  });
1040
- const workspaceProjects = await this.getProjects(workspace.id);
1070
+ const { items: workspaceProjects } = await this.getProjects(workspace.id);
1041
1071
  const foundProject = workspaceProjects.find(p => p.name === projectName);
1042
1072
  if (foundProject) {
1043
1073
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project found by name in different workspace', {
@@ -1092,12 +1122,16 @@ class MotionApiService {
1092
1122
  const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1093
1123
  const lowerQuery = query.toLowerCase();
1094
1124
  const allMatchingTasks = [];
1125
+ let aggregateTruncation;
1095
1126
  // First, search in the specified workspace
1096
- const primaryTasks = await this.getTasks({
1127
+ const { items: primaryTasks, truncation: primaryTruncation } = await this.getTasks({
1097
1128
  workspaceId,
1098
1129
  limit: (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingTasks.length, effectiveLimit),
1099
1130
  maxPages: constants_1.LIMITS.MAX_PAGES
1100
1131
  });
1132
+ if (primaryTruncation?.wasTruncated && !aggregateTruncation) {
1133
+ aggregateTruncation = primaryTruncation;
1134
+ }
1101
1135
  const primaryMatches = primaryTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
1102
1136
  task.description?.toLowerCase().includes(lowerQuery));
1103
1137
  allMatchingTasks.push(...primaryMatches.slice(0, effectiveLimit));
@@ -1128,11 +1162,14 @@ class MotionApiService {
1128
1162
  searchingWorkspaceName: workspace.name,
1129
1163
  remainingNeeded: effectiveLimit - allMatchingTasks.length
1130
1164
  });
1131
- const workspaceTasks = await this.getTasks({
1165
+ const { items: workspaceTasks, truncation: wsTruncation } = await this.getTasks({
1132
1166
  workspaceId: workspace.id,
1133
1167
  limit: fetchLimit,
1134
1168
  maxPages: constants_1.LIMITS.MAX_PAGES
1135
1169
  });
1170
+ if (wsTruncation?.wasTruncated && !aggregateTruncation) {
1171
+ aggregateTruncation = wsTruncation;
1172
+ }
1136
1173
  const workspaceMatches = workspaceTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
1137
1174
  task.description?.toLowerCase().includes(lowerQuery));
1138
1175
  // Only add as many as we still need
@@ -1176,7 +1213,10 @@ class MotionApiService {
1176
1213
  returnedResults: allMatchingTasks.length,
1177
1214
  limit: effectiveLimit
1178
1215
  });
1179
- return allMatchingTasks;
1216
+ if (aggregateTruncation) {
1217
+ aggregateTruncation.returnedCount = allMatchingTasks.length;
1218
+ }
1219
+ return { items: allMatchingTasks, truncation: aggregateTruncation };
1180
1220
  }
1181
1221
  catch (error) {
1182
1222
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search tasks', {
@@ -1199,11 +1239,15 @@ class MotionApiService {
1199
1239
  const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1200
1240
  const lowerQuery = query.toLowerCase();
1201
1241
  const allMatchingProjects = [];
1242
+ let aggregateTruncation;
1202
1243
  // First, search in the specified workspace
1203
- const primaryProjects = await this.getProjects(workspaceId, {
1244
+ const { items: primaryProjects, truncation: primaryTruncation } = await this.getProjects(workspaceId, {
1204
1245
  maxPages: constants_1.LIMITS.MAX_PAGES,
1205
1246
  limit: (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingProjects.length, effectiveLimit)
1206
1247
  });
1248
+ if (primaryTruncation?.wasTruncated && !aggregateTruncation) {
1249
+ aggregateTruncation = primaryTruncation;
1250
+ }
1207
1251
  const primaryMatches = primaryProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
1208
1252
  project.description?.toLowerCase().includes(lowerQuery));
1209
1253
  allMatchingProjects.push(...primaryMatches.slice(0, effectiveLimit));
@@ -1234,10 +1278,13 @@ class MotionApiService {
1234
1278
  searchingWorkspaceName: workspace.name,
1235
1279
  remainingNeeded: effectiveLimit - allMatchingProjects.length
1236
1280
  });
1237
- const workspaceProjects = await this.getProjects(workspace.id, {
1281
+ const { items: workspaceProjects, truncation: wsTruncation } = await this.getProjects(workspace.id, {
1238
1282
  maxPages: constants_1.LIMITS.MAX_PAGES,
1239
1283
  limit: fetchLimit
1240
1284
  });
1285
+ if (wsTruncation?.wasTruncated && !aggregateTruncation) {
1286
+ aggregateTruncation = wsTruncation;
1287
+ }
1241
1288
  const workspaceMatches = workspaceProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
1242
1289
  project.description?.toLowerCase().includes(lowerQuery));
1243
1290
  // Only add as many as we still need
@@ -1281,7 +1328,10 @@ class MotionApiService {
1281
1328
  returnedResults: allMatchingProjects.length,
1282
1329
  limit: effectiveLimit
1283
1330
  });
1284
- return allMatchingProjects;
1331
+ if (aggregateTruncation) {
1332
+ aggregateTruncation.returnedCount = allMatchingProjects.length;
1333
+ }
1334
+ return { items: allMatchingProjects, truncation: aggregateTruncation };
1285
1335
  }
1286
1336
  catch (error) {
1287
1337
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search projects', {
@@ -1664,51 +1714,56 @@ class MotionApiService {
1664
1714
  async getRecurringTasks(workspaceId, options) {
1665
1715
  const { maxPages = 10, limit } = options || {};
1666
1716
  const cacheKey = workspaceId ? `recurring-tasks:workspace:${workspaceId}` : 'recurring-tasks:all';
1667
- return this.recurringTaskCache.withCache(cacheKey, async () => {
1668
- try {
1669
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching recurring tasks from Motion API with pagination', {
1670
- method: 'getRecurringTasks',
1671
- workspaceId,
1672
- maxPages,
1673
- limit
1674
- });
1675
- // Create a fetch function for pagination utility
1676
- const fetchPage = async (cursor) => {
1677
- const params = new URLSearchParams();
1678
- if (workspaceId)
1679
- params.append('workspaceId', workspaceId);
1680
- if (cursor)
1681
- params.append('cursor', cursor);
1682
- const queryString = params.toString();
1683
- const url = queryString ? `/recurring-tasks?${queryString}` : '/recurring-tasks';
1684
- return this.requestWithRetry(() => this.client.get(url));
1685
- };
1686
- // Use pagination utility to fetch all pages
1687
- const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'recurring-tasks', {
1688
- maxPages,
1689
- logProgress: true,
1690
- ...(limit ? { maxItems: limit } : {})
1691
- });
1692
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring tasks fetched successfully with pagination', {
1693
- method: 'getRecurringTasks',
1694
- totalCount: paginatedResult.totalFetched,
1695
- pagesProcessed: Math.ceil(paginatedResult.totalFetched / 50), // Assuming ~50 items per page
1696
- hasMore: paginatedResult.hasMore,
1697
- workspaceId
1698
- });
1699
- return paginatedResult.items;
1700
- }
1701
- catch (error) {
1702
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch recurring tasks', {
1703
- method: 'getRecurringTasks',
1704
- error: getErrorMessage(error),
1705
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1706
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1707
- workspaceId
1708
- });
1709
- throw this.formatApiError(error, 'fetch', 'recurring task');
1710
- }
1711
- });
1717
+ // Check cache - return items only (no stale truncation info)
1718
+ const cachedItems = this.recurringTaskCache.get(cacheKey);
1719
+ if (cachedItems !== null) {
1720
+ return { items: cachedItems };
1721
+ }
1722
+ try {
1723
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching recurring tasks from Motion API with pagination', {
1724
+ method: 'getRecurringTasks',
1725
+ workspaceId,
1726
+ maxPages,
1727
+ limit
1728
+ });
1729
+ // Create a fetch function for pagination utility
1730
+ const fetchPage = async (cursor) => {
1731
+ const params = new URLSearchParams();
1732
+ if (workspaceId)
1733
+ params.append('workspaceId', workspaceId);
1734
+ if (cursor)
1735
+ params.append('cursor', cursor);
1736
+ const queryString = params.toString();
1737
+ const url = queryString ? `/recurring-tasks?${queryString}` : '/recurring-tasks';
1738
+ return this.requestWithRetry(() => this.client.get(url));
1739
+ };
1740
+ // Use pagination utility to fetch all pages
1741
+ const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'recurring-tasks', {
1742
+ maxPages,
1743
+ logProgress: true,
1744
+ ...(limit ? { maxItems: limit } : {})
1745
+ });
1746
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring tasks fetched successfully with pagination', {
1747
+ method: 'getRecurringTasks',
1748
+ totalCount: paginatedResult.totalFetched,
1749
+ pagesProcessed: Math.ceil(paginatedResult.totalFetched / 50), // Assuming ~50 items per page
1750
+ hasMore: paginatedResult.hasMore,
1751
+ workspaceId
1752
+ });
1753
+ // Cache only items, not truncation metadata
1754
+ this.recurringTaskCache.set(cacheKey, paginatedResult.items);
1755
+ return { items: paginatedResult.items, truncation: paginatedResult.truncation };
1756
+ }
1757
+ catch (error) {
1758
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch recurring tasks', {
1759
+ method: 'getRecurringTasks',
1760
+ error: getErrorMessage(error),
1761
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1762
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1763
+ workspaceId
1764
+ });
1765
+ throw this.formatApiError(error, 'fetch', 'recurring task');
1766
+ }
1712
1767
  }
1713
1768
  /**
1714
1769
  * Create a new recurring task
@@ -1953,6 +2008,7 @@ class MotionApiService {
1953
2008
  // Apply limit to prevent resource exhaustion
1954
2009
  const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1955
2010
  const allUncompletedTasks = [];
2011
+ let aggregateTruncation;
1956
2012
  try {
1957
2013
  // Get all workspaces
1958
2014
  const workspaces = await this.getWorkspaces();
@@ -1971,12 +2027,15 @@ class MotionApiService {
1971
2027
  if (fetchLimit <= 0)
1972
2028
  break;
1973
2029
  // Get tasks from this workspace with adaptive limit
1974
- const workspaceTasks = await this.getTasks({
2030
+ const { items: workspaceTasks, truncation: wsTruncation } = await this.getTasks({
1975
2031
  workspaceId: workspace.id,
1976
2032
  assigneeId,
1977
2033
  limit: fetchLimit,
1978
2034
  maxPages: constants_1.LIMITS.MAX_PAGES
1979
2035
  });
2036
+ if (wsTruncation?.wasTruncated && !aggregateTruncation) {
2037
+ aggregateTruncation = wsTruncation;
2038
+ }
1980
2039
  // Filter for uncompleted tasks
1981
2040
  const uncompletedTasks = workspaceTasks.filter(task => {
1982
2041
  // Task is uncompleted if status is missing or isResolvedStatus is false
@@ -2024,7 +2083,10 @@ class MotionApiService {
2024
2083
  returned: allUncompletedTasks.length,
2025
2084
  limit: effectiveLimit
2026
2085
  });
2027
- return allUncompletedTasks;
2086
+ if (aggregateTruncation) {
2087
+ aggregateTruncation.returnedCount = allUncompletedTasks.length;
2088
+ }
2089
+ return { items: allUncompletedTasks, truncation: aggregateTruncation };
2028
2090
  }
2029
2091
  catch (error) {
2030
2092
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch all uncompleted tasks', {