motionmcp 2.1.1 → 2.2.1

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 (66) hide show
  1. package/README.md +1 -1
  2. package/dist/handlers/CommentHandler.d.ts.map +1 -1
  3. package/dist/handlers/CommentHandler.js +3 -5
  4. package/dist/handlers/CommentHandler.js.map +1 -1
  5. package/dist/handlers/TaskHandler.d.ts +72 -0
  6. package/dist/handlers/TaskHandler.d.ts.map +1 -1
  7. package/dist/handlers/TaskHandler.js +77 -1
  8. package/dist/handlers/TaskHandler.js.map +1 -1
  9. package/dist/schemas/motion.d.ts +313 -4259
  10. package/dist/schemas/motion.d.ts.map +1 -1
  11. package/dist/schemas/motion.js +4 -4
  12. package/dist/schemas/motion.js.map +1 -1
  13. package/dist/services/motionApi.d.ts +15 -6
  14. package/dist/services/motionApi.d.ts.map +1 -1
  15. package/dist/services/motionApi.js +195 -108
  16. package/dist/services/motionApi.js.map +1 -1
  17. package/dist/tools/ToolDefinitions.d.ts.map +1 -1
  18. package/dist/tools/ToolDefinitions.js +23 -9
  19. package/dist/tools/ToolDefinitions.js.map +1 -1
  20. package/dist/tools/ToolRegistry.d.ts +50 -0
  21. package/dist/tools/ToolRegistry.d.ts.map +1 -1
  22. package/dist/tools/ToolRegistry.js +51 -6
  23. package/dist/tools/ToolRegistry.js.map +1 -1
  24. package/dist/types/mcp-tool-args.d.ts +2 -7
  25. package/dist/types/mcp-tool-args.d.ts.map +1 -1
  26. package/dist/types/motion.d.ts +11 -7
  27. package/dist/types/motion.d.ts.map +1 -1
  28. package/dist/utils/constants.d.ts +4 -0
  29. package/dist/utils/constants.d.ts.map +1 -1
  30. package/dist/utils/constants.js +6 -1
  31. package/dist/utils/constants.js.map +1 -1
  32. package/dist/utils/errorHandling.d.ts +17 -2
  33. package/dist/utils/errorHandling.d.ts.map +1 -1
  34. package/dist/utils/errorHandling.js +46 -4
  35. package/dist/utils/errorHandling.js.map +1 -1
  36. package/dist/utils/frequencyTransform.d.ts +41 -0
  37. package/dist/utils/frequencyTransform.d.ts.map +1 -0
  38. package/dist/utils/frequencyTransform.js +326 -0
  39. package/dist/utils/frequencyTransform.js.map +1 -0
  40. package/dist/utils/index.d.ts +1 -0
  41. package/dist/utils/index.d.ts.map +1 -1
  42. package/dist/utils/index.js +2 -0
  43. package/dist/utils/index.js.map +1 -1
  44. package/dist/utils/pagination.d.ts +61 -0
  45. package/dist/utils/pagination.d.ts.map +1 -0
  46. package/dist/utils/pagination.js +168 -0
  47. package/dist/utils/pagination.js.map +1 -0
  48. package/dist/utils/paginationNew.d.ts +14 -0
  49. package/dist/utils/paginationNew.d.ts.map +1 -1
  50. package/dist/utils/paginationNew.js +26 -0
  51. package/dist/utils/paginationNew.js.map +1 -1
  52. package/dist/utils/parameterUtils.d.ts.map +1 -1
  53. package/dist/utils/parameterUtils.js +11 -1
  54. package/dist/utils/parameterUtils.js.map +1 -1
  55. package/dist/utils/userFacingErrors.d.ts +49 -0
  56. package/dist/utils/userFacingErrors.d.ts.map +1 -0
  57. package/dist/utils/userFacingErrors.js +174 -0
  58. package/dist/utils/userFacingErrors.js.map +1 -0
  59. package/dist/utils/workspaceResolver.d.ts.map +1 -1
  60. package/dist/utils/workspaceResolver.js +6 -1
  61. package/dist/utils/workspaceResolver.js.map +1 -1
  62. package/package.json +15 -6
  63. package/dist/mcp-server-old.d.ts +0 -29
  64. package/dist/mcp-server-old.d.ts.map +0 -1
  65. package/dist/mcp-server-old.js +0 -1304
  66. package/dist/mcp-server-old.js.map +0 -1
@@ -36,10 +36,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MotionApiService = void 0;
37
37
  const axios_1 = __importStar(require("axios"));
38
38
  const constants_1 = require("../utils/constants");
39
+ const frequencyTransform_1 = require("../utils/frequencyTransform");
39
40
  const logger_1 = require("../utils/logger");
40
41
  const cache_1 = require("../utils/cache");
41
42
  const paginationNew_1 = require("../utils/paginationNew");
42
43
  const responseWrapper_1 = require("../utils/responseWrapper");
44
+ const userFacingErrors_1 = require("../utils/userFacingErrors");
43
45
  const zod_1 = require("zod");
44
46
  const motion_1 = require("../schemas/motion");
45
47
  // Note: Using native axios.isAxiosError instead of custom implementation
@@ -66,7 +68,7 @@ class MotionApiService {
66
68
  if (error instanceof zod_1.z.ZodError) {
67
69
  const errorDetails = {
68
70
  context,
69
- validationErrors: error.errors,
71
+ validationErrors: error.issues,
70
72
  ...(motion_1.VALIDATION_CONFIG.includeDataInLogs ? { receivedData: data } : {})
71
73
  };
72
74
  if (motion_1.VALIDATION_CONFIG.logErrors) {
@@ -101,7 +103,8 @@ class MotionApiService {
101
103
  headers: {
102
104
  'X-API-Key': this.apiKey,
103
105
  'Content-Type': 'application/json'
104
- }
106
+ },
107
+ timeout: constants_1.API_CONFIG.TIMEOUT_MS
105
108
  });
106
109
  // Initialize cache instances with TTL from constants (converted to ms)
107
110
  this.workspaceCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.WORKSPACES);
@@ -135,23 +138,32 @@ class MotionApiService {
135
138
  };
136
139
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Motion API request failed', errorDetails);
137
140
  // Create typed error for better handling
138
- const typedError = new Error(errorData?.message || error.message);
139
- typedError.response = error.response;
141
+ const typedError = Object.assign(new Error(errorData?.message || error.message), {
142
+ response: error.response ? {
143
+ status: error.response.status,
144
+ statusText: error.response.statusText,
145
+ data: error.response.data
146
+ } : undefined
147
+ });
140
148
  throw typedError;
141
149
  });
142
150
  }
143
151
  /**
144
- * Formats API errors consistently across all methods
152
+ * Formats API errors with user-friendly messages while preserving technical details
145
153
  * @param error - The error that occurred
146
- * @param action - Description of the action that failed (e.g., 'fetch projects')
147
- * @returns Formatted Error object
154
+ * @param action - Description of the action that failed (e.g., 'fetch', 'create', 'update')
155
+ * @param resourceType - Type of resource being operated on (e.g., 'project', 'task')
156
+ * @param resourceId - Optional ID of the resource
157
+ * @param resourceName - Optional name of the resource
158
+ * @returns UserFacingError with both user-friendly and technical messages
148
159
  */
149
- formatApiError(error, action) {
150
- const baseMessage = `Failed to ${action}`;
151
- const apiMessage = (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined;
152
- const errorMessage = getErrorMessage(error);
153
- return new Error(`${baseMessage}: ${apiMessage || errorMessage}`);
160
+ formatApiError(error, action, resourceType, resourceId, resourceName) {
161
+ const context = (0, userFacingErrors_1.createErrorContext)(action, resourceType, resourceId, resourceName);
162
+ return (0, userFacingErrors_1.createUserFacingError)(error, context);
154
163
  }
164
+ // ========================================
165
+ // PRIVATE HELPER METHODS
166
+ // ========================================
155
167
  /**
156
168
  * Wraps an axios request with a retry mechanism featuring exponential backoff.
157
169
  * Only retries on 5xx server errors or 429 rate-limiting errors.
@@ -208,14 +220,23 @@ class MotionApiService {
208
220
  // Should never reach here, but TypeScript requires a return or throw
209
221
  throw new Error('Max retries exceeded');
210
222
  }
211
- async getProjects(workspaceId, maxPages = 5) {
223
+ // ========================================
224
+ // PROJECT API METHODS
225
+ // ========================================
226
+ async getProjects(workspaceId, options) {
227
+ const { maxPages = 5, limit } = options || {};
228
+ // Validate limit parameter if provided
229
+ if (limit !== undefined && (limit < 0 || !Number.isInteger(limit))) {
230
+ throw new Error('limit must be a non-negative integer');
231
+ }
212
232
  const cacheKey = `projects:workspace:${workspaceId}`;
213
233
  return this.projectCache.withCache(cacheKey, async () => {
214
234
  try {
215
235
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from Motion API', {
216
236
  method: 'getProjects',
217
237
  workspaceId,
218
- maxPages
238
+ maxPages,
239
+ limit
219
240
  });
220
241
  // Create a fetch function for potential pagination
221
242
  const fetchPage = async (cursor) => {
@@ -232,7 +253,8 @@ class MotionApiService {
232
253
  // Attempt pagination-aware fetch with new response wrapper
233
254
  const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'projects', {
234
255
  maxPages,
235
- logProgress: false
256
+ logProgress: false,
257
+ ...(limit ? { maxItems: limit } : {})
236
258
  });
237
259
  if (paginatedResult.totalFetched > 0) {
238
260
  let projects = paginatedResult.items;
@@ -269,7 +291,7 @@ class MotionApiService {
269
291
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
270
292
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
271
293
  });
272
- throw this.formatApiError(error, 'fetch projects');
294
+ throw this.formatApiError(error, 'fetch', 'project');
273
295
  }
274
296
  });
275
297
  }
@@ -307,7 +329,7 @@ class MotionApiService {
307
329
  method: 'getAllProjects',
308
330
  error: getErrorMessage(error)
309
331
  });
310
- throw this.formatApiError(error, 'fetch all projects');
332
+ throw this.formatApiError(error, 'fetch', 'project');
311
333
  }
312
334
  }
313
335
  async getProject(projectId) {
@@ -334,7 +356,7 @@ class MotionApiService {
334
356
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
335
357
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
336
358
  });
337
- throw this.formatApiError(error, 'fetch project');
359
+ throw this.formatApiError(error, 'fetch', 'project', projectId);
338
360
  }
339
361
  });
340
362
  }
@@ -373,7 +395,7 @@ class MotionApiService {
373
395
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
374
396
  fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
375
397
  });
376
- throw this.formatApiError(error, 'create project');
398
+ throw this.formatApiError(error, 'create', 'project', undefined, projectData.name);
377
399
  }
378
400
  }
379
401
  async updateProject(projectId, updates) {
@@ -403,7 +425,7 @@ class MotionApiService {
403
425
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
404
426
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
405
427
  });
406
- throw this.formatApiError(error, 'update project');
428
+ throw this.formatApiError(error, 'update', 'project', projectId);
407
429
  }
408
430
  }
409
431
  async deleteProject(projectId) {
@@ -428,11 +450,18 @@ class MotionApiService {
428
450
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
429
451
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
430
452
  });
431
- throw this.formatApiError(error, 'delete project');
453
+ throw this.formatApiError(error, 'delete', 'project', projectId);
432
454
  }
433
455
  }
456
+ // ========================================
457
+ // TASK API METHODS
458
+ // ========================================
434
459
  async getTasks(options) {
435
460
  const { workspaceId, projectId, status, assigneeId, priority, dueDate, labels, limit, maxPages = 5 } = options;
461
+ // Validate limit parameter if provided
462
+ if (limit !== undefined && (limit < 0 || !Number.isInteger(limit))) {
463
+ throw new Error('limit must be a non-negative integer');
464
+ }
436
465
  try {
437
466
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching tasks from Motion API', {
438
467
  method: 'getTasks',
@@ -486,21 +515,17 @@ class MotionApiService {
486
515
  ...(limit ? { maxItems: limit } : {})
487
516
  });
488
517
  if (paginatedResult.totalFetched > 0) {
489
- let tasks = paginatedResult.items;
490
- // Apply limit if specified
491
- if (limit && limit > 0) {
492
- tasks = tasks.slice(0, limit);
493
- }
518
+ // Note: limit is already enforced by maxItems in pagination, no need to slice
494
519
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully with pagination', {
495
520
  method: 'getTasks',
496
521
  totalCount: paginatedResult.totalFetched,
497
- returnedCount: tasks.length,
522
+ returnedCount: paginatedResult.items.length,
498
523
  hasMore: paginatedResult.hasMore,
499
524
  workspaceId,
500
525
  projectId,
501
526
  limitApplied: limit
502
527
  });
503
- return tasks;
528
+ return paginatedResult.items;
504
529
  }
505
530
  }
506
531
  catch (paginationError) {
@@ -534,7 +559,7 @@ class MotionApiService {
534
559
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
535
560
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
536
561
  });
537
- throw this.formatApiError(error, 'fetch tasks');
562
+ throw this.formatApiError(error, 'fetch', 'task');
538
563
  }
539
564
  }
540
565
  async getTask(taskId) {
@@ -559,7 +584,7 @@ class MotionApiService {
559
584
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
560
585
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
561
586
  });
562
- throw this.formatApiError(error, 'fetch task');
587
+ throw this.formatApiError(error, 'fetch', 'task', taskId);
563
588
  }
564
589
  }
565
590
  async createTask(taskData) {
@@ -598,7 +623,7 @@ class MotionApiService {
598
623
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
599
624
  fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
600
625
  });
601
- throw this.formatApiError(error, 'create task');
626
+ throw this.formatApiError(error, 'create', 'task', undefined, taskData.name);
602
627
  }
603
628
  }
604
629
  async updateTask(taskId, updates) {
@@ -626,7 +651,7 @@ class MotionApiService {
626
651
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
627
652
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
628
653
  });
629
- throw this.formatApiError(error, 'update task');
654
+ throw this.formatApiError(error, 'update', 'task', taskId);
630
655
  }
631
656
  }
632
657
  async deleteTask(taskId) {
@@ -649,7 +674,7 @@ class MotionApiService {
649
674
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
650
675
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
651
676
  });
652
- throw this.formatApiError(error, 'delete task');
677
+ throw this.formatApiError(error, 'delete', 'task', taskId);
653
678
  }
654
679
  }
655
680
  async moveTask(taskId, targetProjectId, targetWorkspaceId) {
@@ -675,7 +700,7 @@ class MotionApiService {
675
700
  targetProjectId,
676
701
  targetWorkspaceId
677
702
  });
678
- // TODO: Invalidate task cache for source and destination projects/workspaces when implemented
703
+ // Note: No task cache currently implemented - tasks are not cached due to frequent updates
679
704
  return response.data;
680
705
  }
681
706
  catch (error) {
@@ -688,7 +713,7 @@ class MotionApiService {
688
713
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
689
714
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
690
715
  });
691
- throw this.formatApiError(error, 'move task');
716
+ throw this.formatApiError(error, 'move', 'task', taskId);
692
717
  }
693
718
  }
694
719
  async unassignTask(taskId) {
@@ -702,7 +727,7 @@ class MotionApiService {
702
727
  method: 'unassignTask',
703
728
  taskId
704
729
  });
705
- // TODO: Invalidate task cache for this task and any assignee-related caches when implemented
730
+ // Note: No task cache currently implemented - tasks are not cached due to frequent updates
706
731
  return response.data;
707
732
  }
708
733
  catch (error) {
@@ -713,9 +738,12 @@ class MotionApiService {
713
738
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
714
739
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
715
740
  });
716
- throw this.formatApiError(error, 'unassign task');
741
+ throw this.formatApiError(error, 'unassign', 'task', taskId);
717
742
  }
718
743
  }
744
+ // ========================================
745
+ // WORKSPACE API METHODS
746
+ // ========================================
719
747
  async getWorkspaces() {
720
748
  return this.workspaceCache.withCache('workspaces', async () => {
721
749
  try {
@@ -743,10 +771,13 @@ class MotionApiService {
743
771
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
744
772
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
745
773
  });
746
- throw this.formatApiError(error, 'fetch workspaces');
774
+ throw this.formatApiError(error, 'fetch', 'workspace');
747
775
  }
748
776
  });
749
777
  }
778
+ // ========================================
779
+ // USER API METHODS
780
+ // ========================================
750
781
  async getUsers(workspaceId) {
751
782
  const cacheKey = workspaceId ? `users:workspace:${workspaceId}` : 'users:all';
752
783
  return this.userCache.withCache(cacheKey, async () => {
@@ -779,7 +810,7 @@ class MotionApiService {
779
810
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
780
811
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
781
812
  });
782
- throw this.formatApiError(error, 'fetch users');
813
+ throw this.formatApiError(error, 'fetch', 'user');
783
814
  }
784
815
  });
785
816
  }
@@ -807,12 +838,14 @@ class MotionApiService {
807
838
  apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
808
839
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
809
840
  });
810
- throw this.formatApiError(error, 'fetch current user');
841
+ throw this.formatApiError(error, 'fetch', 'user');
811
842
  }
812
843
  });
813
844
  return cachedUsers[0]; // Return just the user object
814
845
  }
815
- // Additional methods for intelligent features
846
+ // ========================================
847
+ // SEARCH AND RESOLUTION METHODS
848
+ // ========================================
816
849
  /**
817
850
  * Resolves a project identifier (either projectId or projectName) to a MotionProject
818
851
  * Searches across all workspaces if not found in the specified workspace
@@ -1058,21 +1091,22 @@ class MotionApiService {
1058
1091
  // Apply search limit to prevent resource exhaustion
1059
1092
  const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1060
1093
  const lowerQuery = query.toLowerCase();
1061
- let allMatchingTasks = [];
1094
+ const allMatchingTasks = [];
1062
1095
  // First, search in the specified workspace
1063
1096
  const primaryTasks = await this.getTasks({
1064
1097
  workspaceId,
1065
- limit: effectiveLimit,
1098
+ limit: (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingTasks.length, effectiveLimit),
1066
1099
  maxPages: constants_1.LIMITS.MAX_PAGES
1067
1100
  });
1068
1101
  const primaryMatches = primaryTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
1069
1102
  task.description?.toLowerCase().includes(lowerQuery));
1070
- allMatchingTasks.push(...primaryMatches);
1103
+ allMatchingTasks.push(...primaryMatches.slice(0, effectiveLimit));
1071
1104
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
1072
1105
  method: 'searchTasks',
1073
1106
  query,
1074
1107
  primaryWorkspaceId: workspaceId,
1075
- primaryMatches: primaryMatches.length
1108
+ primaryMatches: primaryMatches.length,
1109
+ keptMatches: allMatchingTasks.length
1076
1110
  });
1077
1111
  // If we haven't reached the limit, search other workspaces
1078
1112
  if (allMatchingTasks.length < effectiveLimit) {
@@ -1083,27 +1117,35 @@ class MotionApiService {
1083
1117
  if (allMatchingTasks.length >= effectiveLimit)
1084
1118
  break;
1085
1119
  try {
1120
+ // Calculate fetch limit before API call (defense-in-depth)
1121
+ const fetchLimit = (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingTasks.length, effectiveLimit);
1122
+ if (fetchLimit <= 0)
1123
+ break;
1086
1124
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for tasks', {
1087
1125
  method: 'searchTasks',
1088
1126
  query,
1089
1127
  searchingWorkspaceId: workspace.id,
1090
- searchingWorkspaceName: workspace.name
1128
+ searchingWorkspaceName: workspace.name,
1129
+ remainingNeeded: effectiveLimit - allMatchingTasks.length
1091
1130
  });
1092
1131
  const workspaceTasks = await this.getTasks({
1093
1132
  workspaceId: workspace.id,
1094
- limit: effectiveLimit,
1133
+ limit: fetchLimit,
1095
1134
  maxPages: constants_1.LIMITS.MAX_PAGES
1096
1135
  });
1097
1136
  const workspaceMatches = workspaceTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
1098
1137
  task.description?.toLowerCase().includes(lowerQuery));
1099
- allMatchingTasks.push(...workspaceMatches);
1138
+ // Only add as many as we still need
1139
+ const remaining = effectiveLimit - allMatchingTasks.length;
1140
+ allMatchingTasks.push(...workspaceMatches.slice(0, remaining));
1100
1141
  if (workspaceMatches.length > 0) {
1101
1142
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
1102
1143
  method: 'searchTasks',
1103
1144
  query,
1104
1145
  workspaceId: workspace.id,
1105
1146
  workspaceName: workspace.name,
1106
- matches: workspaceMatches.length
1147
+ matches: workspaceMatches.length,
1148
+ keptMatches: Math.min(workspaceMatches.length, remaining)
1107
1149
  });
1108
1150
  }
1109
1151
  }
@@ -1127,17 +1169,14 @@ class MotionApiService {
1127
1169
  });
1128
1170
  }
1129
1171
  }
1130
- // Apply final limit and return results
1131
- const finalResults = allMatchingTasks.slice(0, effectiveLimit);
1172
+ // Results are already limited during collection, no need to slice again
1132
1173
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task search completed across all workspaces', {
1133
1174
  method: 'searchTasks',
1134
1175
  query,
1135
- totalMatches: allMatchingTasks.length,
1136
- returnedResults: finalResults.length,
1137
- limit: effectiveLimit,
1138
- crossWorkspaceSearch: allMatchingTasks.length > primaryMatches.length
1176
+ returnedResults: allMatchingTasks.length,
1177
+ limit: effectiveLimit
1139
1178
  });
1140
- return finalResults;
1179
+ return allMatchingTasks;
1141
1180
  }
1142
1181
  catch (error) {
1143
1182
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search tasks', {
@@ -1159,17 +1198,21 @@ class MotionApiService {
1159
1198
  // Apply search limit to prevent resource exhaustion
1160
1199
  const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1161
1200
  const lowerQuery = query.toLowerCase();
1162
- let allMatchingProjects = [];
1201
+ const allMatchingProjects = [];
1163
1202
  // First, search in the specified workspace
1164
- const primaryProjects = await this.getProjects(workspaceId, constants_1.LIMITS.MAX_PAGES);
1203
+ const primaryProjects = await this.getProjects(workspaceId, {
1204
+ maxPages: constants_1.LIMITS.MAX_PAGES,
1205
+ limit: (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingProjects.length, effectiveLimit)
1206
+ });
1165
1207
  const primaryMatches = primaryProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
1166
1208
  project.description?.toLowerCase().includes(lowerQuery));
1167
- allMatchingProjects.push(...primaryMatches);
1209
+ allMatchingProjects.push(...primaryMatches.slice(0, effectiveLimit));
1168
1210
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
1169
1211
  method: 'searchProjects',
1170
1212
  query,
1171
1213
  primaryWorkspaceId: workspaceId,
1172
- primaryMatches: primaryMatches.length
1214
+ primaryMatches: primaryMatches.length,
1215
+ keptMatches: allMatchingProjects.length
1173
1216
  });
1174
1217
  // If we haven't reached the limit, search other workspaces
1175
1218
  if (allMatchingProjects.length < effectiveLimit) {
@@ -1180,23 +1223,34 @@ class MotionApiService {
1180
1223
  if (allMatchingProjects.length >= effectiveLimit)
1181
1224
  break;
1182
1225
  try {
1226
+ // Calculate fetch limit before API call (defense-in-depth)
1227
+ const fetchLimit = (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingProjects.length, effectiveLimit);
1228
+ if (fetchLimit <= 0)
1229
+ break;
1183
1230
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for projects', {
1184
1231
  method: 'searchProjects',
1185
1232
  query,
1186
1233
  searchingWorkspaceId: workspace.id,
1187
- searchingWorkspaceName: workspace.name
1234
+ searchingWorkspaceName: workspace.name,
1235
+ remainingNeeded: effectiveLimit - allMatchingProjects.length
1236
+ });
1237
+ const workspaceProjects = await this.getProjects(workspace.id, {
1238
+ maxPages: constants_1.LIMITS.MAX_PAGES,
1239
+ limit: fetchLimit
1188
1240
  });
1189
- const workspaceProjects = await this.getProjects(workspace.id, constants_1.LIMITS.MAX_PAGES);
1190
1241
  const workspaceMatches = workspaceProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
1191
1242
  project.description?.toLowerCase().includes(lowerQuery));
1192
- allMatchingProjects.push(...workspaceMatches);
1243
+ // Only add as many as we still need
1244
+ const remaining = effectiveLimit - allMatchingProjects.length;
1245
+ allMatchingProjects.push(...workspaceMatches.slice(0, remaining));
1193
1246
  if (workspaceMatches.length > 0) {
1194
1247
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
1195
1248
  method: 'searchProjects',
1196
1249
  query,
1197
1250
  workspaceId: workspace.id,
1198
1251
  workspaceName: workspace.name,
1199
- matches: workspaceMatches.length
1252
+ matches: workspaceMatches.length,
1253
+ keptMatches: Math.min(workspaceMatches.length, remaining)
1200
1254
  });
1201
1255
  }
1202
1256
  }
@@ -1220,17 +1274,14 @@ class MotionApiService {
1220
1274
  });
1221
1275
  }
1222
1276
  }
1223
- // Apply final limit and return results
1224
- const finalResults = allMatchingProjects.slice(0, effectiveLimit);
1277
+ // Results are already limited during collection, no need to slice again
1225
1278
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project search completed across all workspaces', {
1226
1279
  method: 'searchProjects',
1227
1280
  query,
1228
- totalMatches: allMatchingProjects.length,
1229
- returnedResults: finalResults.length,
1230
- limit: effectiveLimit,
1231
- crossWorkspaceSearch: allMatchingProjects.length > primaryMatches.length
1281
+ returnedResults: allMatchingProjects.length,
1282
+ limit: effectiveLimit
1232
1283
  });
1233
- return finalResults;
1284
+ return allMatchingProjects;
1234
1285
  }
1235
1286
  catch (error) {
1236
1287
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search projects', {
@@ -1241,6 +1292,9 @@ class MotionApiService {
1241
1292
  throw error;
1242
1293
  }
1243
1294
  }
1295
+ // ========================================
1296
+ // COMMENT API METHODS
1297
+ // ========================================
1244
1298
  /**
1245
1299
  * Get comments for a task with proper pagination support
1246
1300
  * @param taskId Task ID to get comments for
@@ -1287,7 +1341,7 @@ class MotionApiService {
1287
1341
  taskId,
1288
1342
  cursor
1289
1343
  });
1290
- throw this.formatApiError(error, 'fetch comments');
1344
+ throw this.formatApiError(error, 'fetch', 'comment');
1291
1345
  }
1292
1346
  });
1293
1347
  }
@@ -1319,9 +1373,12 @@ class MotionApiService {
1319
1373
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1320
1374
  taskId: commentData?.taskId
1321
1375
  });
1322
- throw this.formatApiError(error, 'create comment');
1376
+ throw this.formatApiError(error, 'create', 'comment');
1323
1377
  }
1324
1378
  }
1379
+ // ========================================
1380
+ // CUSTOM FIELD API METHODS
1381
+ // ========================================
1325
1382
  /**
1326
1383
  * Fetch custom fields from Motion API
1327
1384
  * @param workspaceId - Required workspace ID to get custom fields for
@@ -1354,7 +1411,7 @@ class MotionApiService {
1354
1411
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1355
1412
  workspaceId
1356
1413
  });
1357
- throw this.formatApiError(error, 'fetch custom fields');
1414
+ throw this.formatApiError(error, 'fetch', 'custom field');
1358
1415
  }
1359
1416
  });
1360
1417
  }
@@ -1401,7 +1458,7 @@ class MotionApiService {
1401
1458
  fieldName: fieldData?.name,
1402
1459
  workspaceId
1403
1460
  });
1404
- throw this.formatApiError(error, 'create custom field');
1461
+ throw this.formatApiError(error, 'create', 'custom field', undefined, fieldData.name);
1405
1462
  }
1406
1463
  }
1407
1464
  /**
@@ -1436,7 +1493,7 @@ class MotionApiService {
1436
1493
  fieldId,
1437
1494
  workspaceId
1438
1495
  });
1439
- throw this.formatApiError(error, 'delete custom field');
1496
+ throw this.formatApiError(error, 'delete', 'custom field', fieldId);
1440
1497
  }
1441
1498
  }
1442
1499
  /**
@@ -1483,7 +1540,7 @@ class MotionApiService {
1483
1540
  projectId,
1484
1541
  fieldId
1485
1542
  });
1486
- throw this.formatApiError(error, 'add custom field to project');
1543
+ throw this.formatApiError(error, 'update', 'project', projectId);
1487
1544
  }
1488
1545
  }
1489
1546
  /**
@@ -1500,8 +1557,7 @@ class MotionApiService {
1500
1557
  fieldId
1501
1558
  });
1502
1559
  await this.requestWithRetry(() => this.client.delete(`/projects/${projectId}/custom-fields/${fieldId}`));
1503
- // TODO: Invalidate project cache for specific workspace when project data is available
1504
- // For now, invalidate all project caches
1560
+ // Invalidate all project caches since we don't have workspace context here
1505
1561
  this.projectCache.invalidate(`projects:`);
1506
1562
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from project successfully', {
1507
1563
  method: 'removeCustomFieldFromProject',
@@ -1519,7 +1575,7 @@ class MotionApiService {
1519
1575
  projectId,
1520
1576
  fieldId
1521
1577
  });
1522
- throw this.formatApiError(error, 'remove custom field from project');
1578
+ throw this.formatApiError(error, 'update', 'project', projectId);
1523
1579
  }
1524
1580
  }
1525
1581
  /**
@@ -1542,10 +1598,7 @@ class MotionApiService {
1542
1598
  ...(value !== undefined && { value })
1543
1599
  };
1544
1600
  const response = await this.requestWithRetry(() => this.client.post(`/tasks/${taskId}/custom-fields`, requestData));
1545
- // TODO: Invalidate task cache when implemented
1546
- // if (response.data?.workspaceId) {
1547
- // this.taskCache.invalidate(`tasks:workspace:${response.data.workspaceId}`);
1548
- // }
1601
+ // Note: No task cache currently implemented - tasks are not cached due to frequent updates
1549
1602
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to task successfully', {
1550
1603
  method: 'addCustomFieldToTask',
1551
1604
  taskId,
@@ -1562,7 +1615,7 @@ class MotionApiService {
1562
1615
  taskId,
1563
1616
  fieldId
1564
1617
  });
1565
- throw this.formatApiError(error, 'add custom field to task');
1618
+ throw this.formatApiError(error, 'update', 'task', taskId);
1566
1619
  }
1567
1620
  }
1568
1621
  /**
@@ -1579,8 +1632,7 @@ class MotionApiService {
1579
1632
  fieldId
1580
1633
  });
1581
1634
  await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}/custom-fields/${fieldId}`));
1582
- // TODO: Invalidate task cache when implemented
1583
- // this.taskCache.invalidate(`tasks:`);
1635
+ // Note: No task cache currently implemented - tasks are not cached due to frequent updates
1584
1636
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from task successfully', {
1585
1637
  method: 'removeCustomFieldFromTask',
1586
1638
  taskId,
@@ -1597,23 +1649,28 @@ class MotionApiService {
1597
1649
  taskId,
1598
1650
  fieldId
1599
1651
  });
1600
- throw this.formatApiError(error, 'remove custom field from task');
1652
+ throw this.formatApiError(error, 'update', 'task', taskId);
1601
1653
  }
1602
1654
  }
1655
+ // ========================================
1656
+ // RECURRING TASK API METHODS
1657
+ // ========================================
1603
1658
  /**
1604
1659
  * Fetch recurring tasks from Motion API with automatic pagination
1605
1660
  * @param workspaceId - Optional workspace ID to filter recurring tasks
1606
- * @param maxPages - Maximum number of pages to fetch (default: 10)
1661
+ * @param options - Optional configuration for maxPages and limit
1607
1662
  * @returns Array of recurring tasks from all pages
1608
1663
  */
1609
- async getRecurringTasks(workspaceId, maxPages = 10) {
1664
+ async getRecurringTasks(workspaceId, options) {
1665
+ const { maxPages = 10, limit } = options || {};
1610
1666
  const cacheKey = workspaceId ? `recurring-tasks:workspace:${workspaceId}` : 'recurring-tasks:all';
1611
1667
  return this.recurringTaskCache.withCache(cacheKey, async () => {
1612
1668
  try {
1613
1669
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching recurring tasks from Motion API with pagination', {
1614
1670
  method: 'getRecurringTasks',
1615
1671
  workspaceId,
1616
- maxPages
1672
+ maxPages,
1673
+ limit
1617
1674
  });
1618
1675
  // Create a fetch function for pagination utility
1619
1676
  const fetchPage = async (cursor) => {
@@ -1629,7 +1686,8 @@ class MotionApiService {
1629
1686
  // Use pagination utility to fetch all pages
1630
1687
  const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'recurring-tasks', {
1631
1688
  maxPages,
1632
- logProgress: true
1689
+ logProgress: true,
1690
+ ...(limit ? { maxItems: limit } : {})
1633
1691
  });
1634
1692
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring tasks fetched successfully with pagination', {
1635
1693
  method: 'getRecurringTasks',
@@ -1648,7 +1706,7 @@ class MotionApiService {
1648
1706
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1649
1707
  workspaceId
1650
1708
  });
1651
- throw this.formatApiError(error, 'fetch recurring tasks');
1709
+ throw this.formatApiError(error, 'fetch', 'recurring task');
1652
1710
  }
1653
1711
  });
1654
1712
  }
@@ -1659,15 +1717,30 @@ class MotionApiService {
1659
1717
  */
1660
1718
  async createRecurringTask(taskData) {
1661
1719
  try {
1720
+ // Validate frequency object before transformation
1721
+ const freqValidation = (0, frequencyTransform_1.validateFrequencyObject)(taskData.frequency);
1722
+ if (!freqValidation.valid) {
1723
+ throw new Error(`Invalid frequency object: ${freqValidation.reason || 'Unknown reason'}`);
1724
+ }
1725
+ // Transform frequency object to API string format
1726
+ const frequencyString = (0, frequencyTransform_1.transformFrequencyToApiString)(taskData.frequency);
1662
1727
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating recurring task in Motion API', {
1663
1728
  method: 'createRecurringTask',
1664
1729
  name: taskData.name,
1665
1730
  assigneeId: taskData.assigneeId,
1666
- frequency: taskData.frequency.type,
1731
+ frequency: frequencyString,
1732
+ originalFrequency: taskData.frequency,
1667
1733
  workspaceId: taskData.workspaceId
1668
1734
  });
1735
+ // Create payload with transformed frequency while preserving other frequency fields
1736
+ const apiPayload = {
1737
+ ...taskData,
1738
+ frequency: frequencyString,
1739
+ // Preserve endDate and other fields that should be sent separately to the API
1740
+ ...(taskData.frequency.endDate && { endDate: taskData.frequency.endDate })
1741
+ };
1669
1742
  // Create minimal payload by removing empty/null values to avoid validation errors
1670
- const minimalPayload = (0, constants_1.createMinimalPayload)(taskData);
1743
+ const minimalPayload = (0, constants_1.createMinimalPayload)(apiPayload);
1671
1744
  const response = await this.requestWithRetry(() => this.client.post('/recurring-tasks', minimalPayload));
1672
1745
  // Invalidate cache after successful creation
1673
1746
  this.recurringTaskCache.invalidate('recurring-tasks:');
@@ -1686,7 +1759,7 @@ class MotionApiService {
1686
1759
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1687
1760
  taskName: taskData?.name
1688
1761
  });
1689
- throw this.formatApiError(error, 'create recurring task');
1762
+ throw this.formatApiError(error, 'create', 'recurring task', undefined, taskData.name);
1690
1763
  }
1691
1764
  }
1692
1765
  /**
@@ -1717,9 +1790,12 @@ class MotionApiService {
1717
1790
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1718
1791
  recurringTaskId
1719
1792
  });
1720
- throw this.formatApiError(error, 'delete recurring task');
1793
+ throw this.formatApiError(error, 'delete', 'recurring task', recurringTaskId);
1721
1794
  }
1722
1795
  }
1796
+ // ========================================
1797
+ // SCHEDULE API METHODS
1798
+ // ========================================
1723
1799
  /**
1724
1800
  * Get available schedule names for auto-scheduling
1725
1801
  * @param workspaceId - Optional workspace ID to filter schedules (currently unused by Motion API)
@@ -1747,7 +1823,7 @@ class MotionApiService {
1747
1823
  error: getErrorMessage(error),
1748
1824
  workspaceId
1749
1825
  });
1750
- throw this.formatApiError(error, 'fetch available schedule names');
1826
+ throw this.formatApiError(error, 'fetch', 'schedule');
1751
1827
  }
1752
1828
  }
1753
1829
  /**
@@ -1805,10 +1881,13 @@ class MotionApiService {
1805
1881
  startDate,
1806
1882
  endDate
1807
1883
  });
1808
- throw this.formatApiError(error, 'fetch schedules');
1884
+ throw this.formatApiError(error, 'fetch', 'schedule');
1809
1885
  }
1810
1886
  });
1811
1887
  }
1888
+ // ========================================
1889
+ // STATUS API METHODS
1890
+ // ========================================
1812
1891
  /**
1813
1892
  * Retrieves available workflow statuses from Motion
1814
1893
  * @param workspaceId - Optional workspace ID to filter statuses
@@ -1853,10 +1932,13 @@ class MotionApiService {
1853
1932
  apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1854
1933
  workspaceId
1855
1934
  });
1856
- throw this.formatApiError(error, 'fetch statuses');
1935
+ throw this.formatApiError(error, 'fetch', 'status');
1857
1936
  }
1858
1937
  });
1859
1938
  }
1939
+ // ========================================
1940
+ // UTILITY METHODS
1941
+ // ========================================
1860
1942
  /**
1861
1943
  * Get all uncompleted tasks across all workspaces and projects
1862
1944
  * Filters tasks where status.isResolvedStatus is false or undefined
@@ -1883,10 +1965,14 @@ class MotionApiService {
1883
1965
  break; // Stop if we've reached the limit
1884
1966
  }
1885
1967
  try {
1886
- // Get all tasks from this workspace (all projects)
1968
+ // Calculate fetch limit before API call (defense-in-depth)
1969
+ const fetchLimit = (0, paginationNew_1.calculateAdaptiveFetchLimit)(allUncompletedTasks.length, effectiveLimit);
1970
+ if (fetchLimit <= 0)
1971
+ break;
1972
+ // Get tasks from this workspace with adaptive limit
1887
1973
  const workspaceTasks = await this.getTasks({
1888
1974
  workspaceId: workspace.id,
1889
- limit: effectiveLimit,
1975
+ limit: fetchLimit,
1890
1976
  maxPages: constants_1.LIMITS.MAX_PAGES
1891
1977
  });
1892
1978
  // Filter for uncompleted tasks
@@ -1898,13 +1984,16 @@ class MotionApiService {
1898
1984
  return true; // Simple string status = assume not resolved
1899
1985
  return !task.status.isResolvedStatus; // Object status with isResolvedStatus false
1900
1986
  });
1901
- allUncompletedTasks.push(...uncompletedTasks);
1987
+ // Only add as many as we still need
1988
+ const remaining = effectiveLimit - allUncompletedTasks.length;
1989
+ allUncompletedTasks.push(...uncompletedTasks.slice(0, remaining));
1902
1990
  if (uncompletedTasks.length > 0) {
1903
1991
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found uncompleted tasks in workspace', {
1904
1992
  method: 'getAllUncompletedTasks',
1905
1993
  workspaceId: workspace.id,
1906
1994
  workspaceName: workspace.name,
1907
1995
  uncompletedTasks: uncompletedTasks.length,
1996
+ keptTasks: Math.min(uncompletedTasks.length, remaining),
1908
1997
  totalTasks: workspaceTasks.length
1909
1998
  });
1910
1999
  }
@@ -1927,15 +2016,13 @@ class MotionApiService {
1927
2016
  });
1928
2017
  throw workspaceListError;
1929
2018
  }
1930
- // Apply final limit and return results
1931
- const finalResults = allUncompletedTasks.slice(0, effectiveLimit);
2019
+ // Results are already limited during collection, no need to slice again
1932
2020
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'All uncompleted tasks fetched successfully', {
1933
2021
  method: 'getAllUncompletedTasks',
1934
- totalFound: allUncompletedTasks.length,
1935
- returned: finalResults.length,
2022
+ returned: allUncompletedTasks.length,
1936
2023
  limit: effectiveLimit
1937
2024
  });
1938
- return finalResults;
2025
+ return allUncompletedTasks;
1939
2026
  }
1940
2027
  catch (error) {
1941
2028
  (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch all uncompleted tasks', {