motionmcp 1.0.1 → 2.0.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 (159) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +251 -417
  3. package/dist/handlers/CommentHandler.d.ts +9 -0
  4. package/dist/handlers/CommentHandler.d.ts.map +1 -0
  5. package/dist/handlers/CommentHandler.js +66 -0
  6. package/dist/handlers/CommentHandler.js.map +1 -0
  7. package/dist/handlers/CustomFieldHandler.d.ts +14 -0
  8. package/dist/handlers/CustomFieldHandler.d.ts.map +1 -0
  9. package/dist/handlers/CustomFieldHandler.js +95 -0
  10. package/dist/handlers/CustomFieldHandler.js.map +1 -0
  11. package/dist/handlers/HandlerFactory.d.ts +15 -0
  12. package/dist/handlers/HandlerFactory.d.ts.map +1 -0
  13. package/dist/handlers/HandlerFactory.js +58 -0
  14. package/dist/handlers/HandlerFactory.js.map +1 -0
  15. package/dist/handlers/ProjectHandler.d.ts +10 -0
  16. package/dist/handlers/ProjectHandler.d.ts.map +1 -0
  17. package/dist/handlers/ProjectHandler.js +63 -0
  18. package/dist/handlers/ProjectHandler.js.map +1 -0
  19. package/dist/handlers/RecurringTaskHandler.d.ts +10 -0
  20. package/dist/handlers/RecurringTaskHandler.d.ts.map +1 -0
  21. package/dist/handlers/RecurringTaskHandler.js +68 -0
  22. package/dist/handlers/RecurringTaskHandler.js.map +1 -0
  23. package/dist/handlers/ScheduleHandler.d.ts +8 -0
  24. package/dist/handlers/ScheduleHandler.d.ts.map +1 -0
  25. package/dist/handlers/ScheduleHandler.js +43 -0
  26. package/dist/handlers/ScheduleHandler.js.map +1 -0
  27. package/dist/handlers/SearchHandler.d.ts +10 -0
  28. package/dist/handlers/SearchHandler.d.ts.map +1 -0
  29. package/dist/handlers/SearchHandler.js +116 -0
  30. package/dist/handlers/SearchHandler.js.map +1 -0
  31. package/dist/handlers/StatusHandler.d.ts +8 -0
  32. package/dist/handlers/StatusHandler.d.ts.map +1 -0
  33. package/dist/handlers/StatusHandler.js +22 -0
  34. package/dist/handlers/StatusHandler.js.map +1 -0
  35. package/dist/handlers/TaskHandler.d.ts +22 -0
  36. package/dist/handlers/TaskHandler.d.ts.map +1 -0
  37. package/dist/handlers/TaskHandler.js +266 -0
  38. package/dist/handlers/TaskHandler.js.map +1 -0
  39. package/dist/handlers/UserHandler.d.ts +9 -0
  40. package/dist/handlers/UserHandler.d.ts.map +1 -0
  41. package/dist/handlers/UserHandler.js +36 -0
  42. package/dist/handlers/UserHandler.js.map +1 -0
  43. package/dist/handlers/WorkspaceHandler.d.ts +10 -0
  44. package/dist/handlers/WorkspaceHandler.d.ts.map +1 -0
  45. package/dist/handlers/WorkspaceHandler.js +49 -0
  46. package/dist/handlers/WorkspaceHandler.js.map +1 -0
  47. package/dist/handlers/base/BaseHandler.d.ts +16 -0
  48. package/dist/handlers/base/BaseHandler.d.ts.map +1 -0
  49. package/dist/handlers/base/BaseHandler.js +31 -0
  50. package/dist/handlers/base/BaseHandler.js.map +1 -0
  51. package/dist/handlers/base/HandlerInterface.d.ts +18 -0
  52. package/dist/handlers/base/HandlerInterface.d.ts.map +1 -0
  53. package/dist/handlers/base/HandlerInterface.js +3 -0
  54. package/dist/handlers/base/HandlerInterface.js.map +1 -0
  55. package/dist/handlers/index.d.ts +14 -0
  56. package/dist/handlers/index.d.ts.map +1 -0
  57. package/dist/handlers/index.js +31 -0
  58. package/dist/handlers/index.js.map +1 -0
  59. package/dist/mcp-server-old.d.ts +29 -0
  60. package/dist/mcp-server-old.d.ts.map +1 -0
  61. package/dist/mcp-server-old.js +1304 -0
  62. package/dist/mcp-server-old.js.map +1 -0
  63. package/dist/mcp-server.d.ts +15 -0
  64. package/dist/mcp-server.d.ts.map +1 -0
  65. package/dist/mcp-server.js +145 -0
  66. package/dist/mcp-server.js.map +1 -0
  67. package/dist/schemas/motion.d.ts +4971 -0
  68. package/dist/schemas/motion.d.ts.map +1 -0
  69. package/dist/schemas/motion.js +328 -0
  70. package/dist/schemas/motion.js.map +1 -0
  71. package/dist/services/motionApi.d.ts +186 -0
  72. package/dist/services/motionApi.d.ts.map +1 -0
  73. package/dist/services/motionApi.js +1912 -0
  74. package/dist/services/motionApi.js.map +1 -0
  75. package/dist/tools/ToolConfigurator.d.ts +19 -0
  76. package/dist/tools/ToolConfigurator.d.ts.map +1 -0
  77. package/dist/tools/ToolConfigurator.js +89 -0
  78. package/dist/tools/ToolConfigurator.js.map +1 -0
  79. package/dist/tools/ToolDefinitions.d.ts +25 -0
  80. package/dist/tools/ToolDefinitions.d.ts.map +1 -0
  81. package/dist/tools/ToolDefinitions.js +502 -0
  82. package/dist/tools/ToolDefinitions.js.map +1 -0
  83. package/dist/tools/ToolRegistry.d.ts +16 -0
  84. package/dist/tools/ToolRegistry.d.ts.map +1 -0
  85. package/dist/tools/ToolRegistry.js +89 -0
  86. package/dist/tools/ToolRegistry.js.map +1 -0
  87. package/dist/tools/index.d.ts +4 -0
  88. package/dist/tools/index.d.ts.map +1 -0
  89. package/dist/tools/index.js +21 -0
  90. package/dist/tools/index.js.map +1 -0
  91. package/dist/types/mcp-tool-args.d.ts +122 -0
  92. package/dist/types/mcp-tool-args.d.ts.map +1 -0
  93. package/dist/types/mcp-tool-args.js +7 -0
  94. package/dist/types/mcp-tool-args.js.map +1 -0
  95. package/dist/types/mcp.d.ts +32 -0
  96. package/dist/types/mcp.d.ts.map +1 -0
  97. package/dist/types/mcp.js +3 -0
  98. package/dist/types/mcp.js.map +1 -0
  99. package/dist/types/motion.d.ts +304 -0
  100. package/dist/types/motion.d.ts.map +1 -0
  101. package/dist/types/motion.js +3 -0
  102. package/dist/types/motion.js.map +1 -0
  103. package/dist/utils/cache.d.ts +25 -0
  104. package/dist/utils/cache.d.ts.map +1 -0
  105. package/dist/utils/cache.js +135 -0
  106. package/dist/utils/cache.js.map +1 -0
  107. package/dist/utils/constants.d.ts +83 -0
  108. package/dist/utils/constants.d.ts.map +1 -0
  109. package/dist/utils/constants.js +151 -0
  110. package/dist/utils/constants.js.map +1 -0
  111. package/dist/utils/errorHandling.d.ts +50 -0
  112. package/dist/utils/errorHandling.d.ts.map +1 -0
  113. package/dist/utils/errorHandling.js +86 -0
  114. package/dist/utils/errorHandling.js.map +1 -0
  115. package/dist/utils/index.d.ts +13 -0
  116. package/dist/utils/index.d.ts.map +1 -0
  117. package/dist/utils/index.js +38 -0
  118. package/dist/utils/index.js.map +1 -0
  119. package/dist/utils/logger.d.ts +13 -0
  120. package/dist/utils/logger.d.ts.map +1 -0
  121. package/dist/utils/logger.js +47 -0
  122. package/dist/utils/logger.js.map +1 -0
  123. package/dist/utils/paginationNew.d.ts +44 -0
  124. package/dist/utils/paginationNew.d.ts.map +1 -0
  125. package/dist/utils/paginationNew.js +149 -0
  126. package/dist/utils/paginationNew.js.map +1 -0
  127. package/dist/utils/parameterUtils.d.ts +79 -0
  128. package/dist/utils/parameterUtils.d.ts.map +1 -0
  129. package/dist/utils/parameterUtils.js +189 -0
  130. package/dist/utils/parameterUtils.js.map +1 -0
  131. package/dist/utils/responseFormatters.d.ts +92 -0
  132. package/dist/utils/responseFormatters.d.ts.map +1 -0
  133. package/dist/utils/responseFormatters.js +331 -0
  134. package/dist/utils/responseFormatters.js.map +1 -0
  135. package/dist/utils/responseWrapper.d.ts +38 -0
  136. package/dist/utils/responseWrapper.d.ts.map +1 -0
  137. package/dist/utils/responseWrapper.js +201 -0
  138. package/dist/utils/responseWrapper.js.map +1 -0
  139. package/dist/utils/sanitize.d.ts +51 -0
  140. package/dist/utils/sanitize.d.ts.map +1 -0
  141. package/dist/utils/sanitize.js +137 -0
  142. package/dist/utils/sanitize.js.map +1 -0
  143. package/dist/utils/validator.d.ts +37 -0
  144. package/dist/utils/validator.d.ts.map +1 -0
  145. package/dist/utils/validator.js +74 -0
  146. package/dist/utils/validator.js.map +1 -0
  147. package/dist/utils/workspaceResolver.d.ts +40 -0
  148. package/dist/utils/workspaceResolver.d.ts.map +1 -0
  149. package/dist/utils/workspaceResolver.js +207 -0
  150. package/dist/utils/workspaceResolver.js.map +1 -0
  151. package/package.json +41 -17
  152. package/.claude/settings.local.json +0 -15
  153. package/.env.example +0 -3
  154. package/sample.png +0 -0
  155. package/src/index.js +0 -49
  156. package/src/mcp-server.js +0 -1137
  157. package/src/routes/motion.js +0 -152
  158. package/src/services/motionApi.js +0 -1177
  159. package/src/worker.js +0 -248
@@ -0,0 +1,1912 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MotionApiService = void 0;
37
+ const axios_1 = __importStar(require("axios"));
38
+ const constants_1 = require("../utils/constants");
39
+ const logger_1 = require("../utils/logger");
40
+ const cache_1 = require("../utils/cache");
41
+ const paginationNew_1 = require("../utils/paginationNew");
42
+ const responseWrapper_1 = require("../utils/responseWrapper");
43
+ const zod_1 = require("zod");
44
+ const motion_1 = require("../schemas/motion");
45
+ // Note: Using native axios.isAxiosError instead of custom implementation
46
+ // Helper to get error message
47
+ function getErrorMessage(error) {
48
+ if (error instanceof Error) {
49
+ return error.message;
50
+ }
51
+ return String(error);
52
+ }
53
+ class MotionApiService {
54
+ /**
55
+ * Validate API response against schema
56
+ * Handles strict/lenient/off modes based on configuration
57
+ */
58
+ validateResponse(data, schema, context) {
59
+ if (motion_1.VALIDATION_CONFIG.mode === 'off') {
60
+ return data;
61
+ }
62
+ try {
63
+ return schema.parse(data);
64
+ }
65
+ catch (error) {
66
+ if (error instanceof zod_1.z.ZodError) {
67
+ const errorDetails = {
68
+ context,
69
+ validationErrors: error.errors,
70
+ ...(motion_1.VALIDATION_CONFIG.includeDataInLogs ? { receivedData: data } : {})
71
+ };
72
+ if (motion_1.VALIDATION_CONFIG.logErrors) {
73
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, `API response validation failed for ${context}`, errorDetails);
74
+ }
75
+ if (motion_1.VALIDATION_CONFIG.mode === 'strict') {
76
+ throw new Error(`Invalid API response structure for ${context}: ${error.message}`);
77
+ }
78
+ // Lenient mode: return original data and hope for the best
79
+ return data;
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+ constructor() {
85
+ const apiKey = process.env.MOTION_API_KEY;
86
+ if (!apiKey) {
87
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Motion API key not found in environment variables', {
88
+ component: 'MotionApiService',
89
+ method: 'constructor'
90
+ });
91
+ throw new Error('MOTION_API_KEY environment variable is required');
92
+ }
93
+ this.apiKey = apiKey;
94
+ this.baseUrl = 'https://api.usemotion.com/v1';
95
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Initializing Motion API service', {
96
+ component: 'MotionApiService',
97
+ baseUrl: this.baseUrl
98
+ });
99
+ this.client = axios_1.default.create({
100
+ baseURL: this.baseUrl,
101
+ headers: {
102
+ 'X-API-Key': this.apiKey,
103
+ 'Content-Type': 'application/json'
104
+ }
105
+ });
106
+ // Initialize cache instances with TTL from constants (converted to ms)
107
+ this.workspaceCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.WORKSPACES * constants_1.CACHE_TTL_MS_MULTIPLIER);
108
+ this.userCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.USERS * constants_1.CACHE_TTL_MS_MULTIPLIER);
109
+ this.projectCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.PROJECTS * constants_1.CACHE_TTL_MS_MULTIPLIER);
110
+ this.singleProjectCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.PROJECTS * constants_1.CACHE_TTL_MS_MULTIPLIER);
111
+ this.commentCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.COMMENTS * constants_1.CACHE_TTL_MS_MULTIPLIER);
112
+ this.customFieldCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.CUSTOM_FIELDS * constants_1.CACHE_TTL_MS_MULTIPLIER);
113
+ this.recurringTaskCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.RECURRING_TASKS * constants_1.CACHE_TTL_MS_MULTIPLIER);
114
+ this.scheduleCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.SCHEDULES * constants_1.CACHE_TTL_MS_MULTIPLIER);
115
+ this.statusCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.WORKSPACES * constants_1.CACHE_TTL_MS_MULTIPLIER); // 10 minutes, like workspaces
116
+ this.client.interceptors.response.use((response) => {
117
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Motion API response successful', {
118
+ url: response.config?.url,
119
+ method: response.config?.method?.toUpperCase(),
120
+ status: response.status,
121
+ component: 'MotionApiService'
122
+ });
123
+ return response;
124
+ }, (error) => {
125
+ const errorData = error.response?.data;
126
+ const errorDetails = {
127
+ url: error.config?.url,
128
+ method: error.config?.method?.toUpperCase(),
129
+ status: error.response?.status,
130
+ statusText: error.response?.statusText,
131
+ apiMessage: errorData?.message,
132
+ apiCode: errorData?.code,
133
+ errorMessage: error.message,
134
+ component: 'MotionApiService'
135
+ };
136
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Motion API request failed', errorDetails);
137
+ // Create typed error for better handling
138
+ const typedError = new Error(errorData?.message || error.message);
139
+ typedError.response = error.response;
140
+ throw typedError;
141
+ });
142
+ }
143
+ /**
144
+ * Formats API errors consistently across all methods
145
+ * @param error - The error that occurred
146
+ * @param action - Description of the action that failed (e.g., 'fetch projects')
147
+ * @returns Formatted Error object
148
+ */
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}`);
154
+ }
155
+ /**
156
+ * Wraps an axios request with a retry mechanism featuring exponential backoff.
157
+ * Only retries on 5xx server errors or 429 rate-limiting errors.
158
+ */
159
+ async requestWithRetry(request) {
160
+ for (let attempt = 1; attempt <= constants_1.RETRY_CONFIG.MAX_RETRIES; attempt++) {
161
+ try {
162
+ return await request();
163
+ }
164
+ catch (error) {
165
+ if (!axios_1.default.isAxiosError(error)) {
166
+ throw error; // Not a network error, re-throw immediately
167
+ }
168
+ const status = error.response?.status;
169
+ const isRetryable = (status && status >= 500) || status === 429;
170
+ if (!isRetryable || attempt === constants_1.RETRY_CONFIG.MAX_RETRIES) {
171
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, `Request failed and will not be retried`, {
172
+ status,
173
+ attempt,
174
+ maxRetries: constants_1.RETRY_CONFIG.MAX_RETRIES,
175
+ isRetryable,
176
+ component: 'MotionApiService',
177
+ method: 'requestWithRetry'
178
+ });
179
+ throw error; // Final attempt failed or error is not retryable
180
+ }
181
+ // Handle Retry-After header for 429
182
+ const retryAfterHeader = error.response?.headers['retry-after'];
183
+ let delay = 0;
184
+ if (status === 429 && retryAfterHeader) {
185
+ const retryAfterSeconds = parseInt(retryAfterHeader, 10);
186
+ if (!isNaN(retryAfterSeconds)) {
187
+ delay = retryAfterSeconds * 1000;
188
+ }
189
+ }
190
+ // If no Retry-After header, use exponential backoff with jitter
191
+ if (delay === 0) {
192
+ const backoff = constants_1.RETRY_CONFIG.INITIAL_BACKOFF_MS * Math.pow(constants_1.RETRY_CONFIG.BACKOFF_MULTIPLIER, attempt - 1);
193
+ const jitter = backoff * constants_1.RETRY_CONFIG.JITTER_FACTOR * Math.random();
194
+ delay = Math.min(backoff + jitter, constants_1.RETRY_CONFIG.MAX_BACKOFF_MS);
195
+ }
196
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, `Request failed, retrying`, {
197
+ attempt,
198
+ maxRetries: constants_1.RETRY_CONFIG.MAX_RETRIES,
199
+ delayMs: Math.round(delay),
200
+ error: error.message,
201
+ status,
202
+ component: 'MotionApiService',
203
+ method: 'requestWithRetry'
204
+ });
205
+ await new Promise(resolve => setTimeout(resolve, delay));
206
+ }
207
+ }
208
+ // Should never reach here, but TypeScript requires a return or throw
209
+ throw new Error('Max retries exceeded');
210
+ }
211
+ async getProjects(workspaceId, maxPages = 5) {
212
+ const cacheKey = `projects:workspace:${workspaceId}`;
213
+ return this.projectCache.withCache(cacheKey, async () => {
214
+ try {
215
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from Motion API', {
216
+ method: 'getProjects',
217
+ workspaceId,
218
+ maxPages
219
+ });
220
+ // Create a fetch function for potential pagination
221
+ const fetchPage = async (cursor) => {
222
+ const params = new URLSearchParams();
223
+ params.append('workspaceId', workspaceId);
224
+ if (cursor) {
225
+ params.append('cursor', cursor);
226
+ }
227
+ const queryString = params.toString();
228
+ const url = `/projects?${queryString}`;
229
+ return this.requestWithRetry(() => this.client.get(url));
230
+ };
231
+ try {
232
+ // Attempt pagination-aware fetch with new response wrapper
233
+ const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'projects', {
234
+ maxPages,
235
+ logProgress: false
236
+ });
237
+ if (paginatedResult.totalFetched > 0) {
238
+ let projects = paginatedResult.items;
239
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully with pagination', {
240
+ method: 'getProjects',
241
+ totalCount: projects.length,
242
+ hasMore: paginatedResult.hasMore,
243
+ workspaceId
244
+ });
245
+ return projects;
246
+ }
247
+ }
248
+ catch (paginationError) {
249
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
250
+ method: 'getProjects',
251
+ error: paginationError instanceof Error ? paginationError.message : String(paginationError)
252
+ });
253
+ }
254
+ // Use new response wrapper for single page fallback
255
+ const response = await fetchPage();
256
+ const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'projects');
257
+ let projects = unwrapped.data;
258
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully (single page)', {
259
+ method: 'getProjects',
260
+ count: projects.length,
261
+ workspaceId
262
+ });
263
+ return projects;
264
+ }
265
+ catch (error) {
266
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects', {
267
+ method: 'getProjects',
268
+ error: getErrorMessage(error),
269
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
270
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
271
+ });
272
+ throw this.formatApiError(error, 'fetch projects');
273
+ }
274
+ });
275
+ }
276
+ async getAllProjects() {
277
+ try {
278
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from all workspaces', {
279
+ method: 'getAllProjects'
280
+ });
281
+ const allWorkspaces = await this.getWorkspaces();
282
+ const allProjects = [];
283
+ for (const workspace of allWorkspaces) {
284
+ try {
285
+ const workspaceProjects = await this.getProjects(workspace.id);
286
+ allProjects.push(...workspaceProjects);
287
+ }
288
+ catch (workspaceError) {
289
+ // Log error but continue with other workspaces
290
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to fetch projects from workspace', {
291
+ method: 'getAllProjects',
292
+ workspaceId: workspace.id,
293
+ workspaceName: workspace.name,
294
+ error: getErrorMessage(workspaceError)
295
+ });
296
+ }
297
+ }
298
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'All projects fetched successfully', {
299
+ method: 'getAllProjects',
300
+ totalProjects: allProjects.length,
301
+ workspaceCount: allWorkspaces.length
302
+ });
303
+ return allProjects;
304
+ }
305
+ catch (error) {
306
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects from all workspaces', {
307
+ method: 'getAllProjects',
308
+ error: getErrorMessage(error)
309
+ });
310
+ throw this.formatApiError(error, 'fetch all projects');
311
+ }
312
+ }
313
+ async getProject(projectId) {
314
+ const cacheKey = `project:${projectId}`;
315
+ return this.singleProjectCache.withCache(cacheKey, async () => {
316
+ try {
317
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching single project from Motion API', {
318
+ method: 'getProject',
319
+ projectId
320
+ });
321
+ const response = await this.requestWithRetry(() => this.client.get(`/projects/${projectId}`));
322
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Successfully fetched project', {
323
+ method: 'getProject',
324
+ projectId,
325
+ projectName: response.data.name
326
+ });
327
+ return response.data;
328
+ }
329
+ catch (error) {
330
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch project', {
331
+ method: 'getProject',
332
+ projectId,
333
+ error: getErrorMessage(error),
334
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
335
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
336
+ });
337
+ throw this.formatApiError(error, 'fetch project');
338
+ }
339
+ });
340
+ }
341
+ async createProject(projectData) {
342
+ try {
343
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating project in Motion API', {
344
+ method: 'createProject',
345
+ projectName: projectData.name,
346
+ workspaceId: projectData.workspaceId
347
+ });
348
+ if (!projectData.workspaceId) {
349
+ throw new Error('Workspace ID is required to create a project');
350
+ }
351
+ // Create minimal payload by removing empty/null values to avoid validation errors
352
+ const minimalPayload = (0, constants_1.createMinimalPayload)(projectData);
353
+ // Debug logging: log the exact payload being sent to API
354
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'API payload for project creation', {
355
+ method: 'createProject',
356
+ payload: JSON.stringify(minimalPayload, null, 2)
357
+ });
358
+ const response = await this.requestWithRetry(() => this.client.post('/projects', minimalPayload));
359
+ // Invalidate cache after successful creation
360
+ this.projectCache.invalidate(`projects:workspace:${projectData.workspaceId}`);
361
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project created successfully', {
362
+ method: 'createProject',
363
+ projectId: response.data.id,
364
+ projectName: response.data.name
365
+ });
366
+ return response.data;
367
+ }
368
+ catch (error) {
369
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create project', {
370
+ method: 'createProject',
371
+ error: getErrorMessage(error),
372
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
373
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
374
+ fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
375
+ });
376
+ throw this.formatApiError(error, 'create project');
377
+ }
378
+ }
379
+ async updateProject(projectId, updates) {
380
+ try {
381
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Updating project in Motion API', {
382
+ method: 'updateProject',
383
+ projectId,
384
+ updates: Object.keys(updates)
385
+ });
386
+ // Create minimal payload by removing empty/null values to avoid validation errors
387
+ const minimalUpdates = (0, constants_1.createMinimalPayload)(updates);
388
+ const response = await this.requestWithRetry(() => this.client.patch(`/projects/${projectId}`, minimalUpdates));
389
+ // Invalidate cache after successful update
390
+ this.projectCache.invalidate(`projects:workspace:${response.data.workspaceId}`);
391
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project updated successfully', {
392
+ method: 'updateProject',
393
+ projectId,
394
+ projectName: response.data.name
395
+ });
396
+ return response.data;
397
+ }
398
+ catch (error) {
399
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to update project', {
400
+ method: 'updateProject',
401
+ projectId,
402
+ error: getErrorMessage(error),
403
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
404
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
405
+ });
406
+ throw this.formatApiError(error, 'update project');
407
+ }
408
+ }
409
+ async deleteProject(projectId) {
410
+ try {
411
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting project from Motion API', {
412
+ method: 'deleteProject',
413
+ projectId
414
+ });
415
+ await this.requestWithRetry(() => this.client.delete(`/projects/${projectId}`));
416
+ // Invalidate all project caches since we don't know the workspace ID
417
+ this.projectCache.invalidate();
418
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project deleted successfully', {
419
+ method: 'deleteProject',
420
+ projectId
421
+ });
422
+ }
423
+ catch (error) {
424
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete project', {
425
+ method: 'deleteProject',
426
+ projectId,
427
+ error: getErrorMessage(error),
428
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
429
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
430
+ });
431
+ throw this.formatApiError(error, 'delete project');
432
+ }
433
+ }
434
+ async getTasks(workspaceId, projectId, maxPages = 5, limit) {
435
+ try {
436
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching tasks from Motion API', {
437
+ method: 'getTasks',
438
+ workspaceId,
439
+ projectId,
440
+ maxPages
441
+ });
442
+ // Create a fetch function for potential pagination
443
+ const fetchPage = async (cursor) => {
444
+ const params = new URLSearchParams();
445
+ params.append('workspaceId', workspaceId);
446
+ if (projectId) {
447
+ params.append('projectId', projectId);
448
+ }
449
+ if (cursor) {
450
+ params.append('cursor', cursor);
451
+ }
452
+ const queryString = params.toString();
453
+ const url = queryString ? `/tasks?${queryString}` : '/tasks';
454
+ return this.requestWithRetry(() => this.client.get(url));
455
+ };
456
+ try {
457
+ // Attempt pagination-aware fetch with new response wrapper
458
+ const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'tasks', {
459
+ maxPages,
460
+ logProgress: false // Less verbose for tasks
461
+ });
462
+ if (paginatedResult.totalFetched > 0) {
463
+ let tasks = paginatedResult.items;
464
+ // Apply limit if specified
465
+ if (limit && limit > 0) {
466
+ tasks = tasks.slice(0, limit);
467
+ }
468
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully with pagination', {
469
+ method: 'getTasks',
470
+ totalCount: paginatedResult.totalFetched,
471
+ returnedCount: tasks.length,
472
+ hasMore: paginatedResult.hasMore,
473
+ workspaceId,
474
+ projectId,
475
+ limitApplied: limit
476
+ });
477
+ return tasks;
478
+ }
479
+ }
480
+ catch (paginationError) {
481
+ // Fallback to simple fetch if pagination fails
482
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
483
+ method: 'getTasks',
484
+ error: paginationError instanceof Error ? paginationError.message : String(paginationError)
485
+ });
486
+ }
487
+ // Use new response wrapper for single page fallback
488
+ const response = await fetchPage();
489
+ const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'tasks');
490
+ let tasks = unwrapped.data;
491
+ // Apply limit if specified
492
+ if (limit && limit > 0) {
493
+ tasks = tasks.slice(0, limit);
494
+ }
495
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully (single page)', {
496
+ method: 'getTasks',
497
+ count: tasks.length,
498
+ workspaceId,
499
+ projectId,
500
+ limitApplied: limit
501
+ });
502
+ return tasks;
503
+ }
504
+ catch (error) {
505
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch tasks', {
506
+ method: 'getTasks',
507
+ error: getErrorMessage(error),
508
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
509
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
510
+ });
511
+ throw this.formatApiError(error, 'fetch tasks');
512
+ }
513
+ }
514
+ async getTask(taskId) {
515
+ try {
516
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching single task from Motion API', {
517
+ method: 'getTask',
518
+ taskId
519
+ });
520
+ const response = await this.requestWithRetry(() => this.client.get(`/tasks/${taskId}`));
521
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Successfully fetched task', {
522
+ method: 'getTask',
523
+ taskId,
524
+ taskName: response.data.name
525
+ });
526
+ return response.data;
527
+ }
528
+ catch (error) {
529
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch task', {
530
+ method: 'getTask',
531
+ taskId,
532
+ error: getErrorMessage(error),
533
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
534
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
535
+ });
536
+ throw this.formatApiError(error, 'fetch task');
537
+ }
538
+ }
539
+ async createTask(taskData) {
540
+ try {
541
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating task in Motion API', {
542
+ method: 'createTask',
543
+ taskName: taskData.name,
544
+ workspaceId: taskData.workspaceId,
545
+ projectId: taskData.projectId
546
+ });
547
+ if (!taskData.workspaceId) {
548
+ throw new Error('Workspace ID is required to create a task');
549
+ }
550
+ // Create minimal payload by removing empty/null values to avoid validation errors
551
+ const minimalPayload = (0, constants_1.createMinimalPayload)(taskData);
552
+ // Debug logging: log the exact payload being sent to API
553
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'API payload for task creation', {
554
+ method: 'createTask',
555
+ payload: JSON.stringify(minimalPayload, null, 2)
556
+ });
557
+ const response = await this.requestWithRetry(() => this.client.post('/tasks', minimalPayload));
558
+ // Invalidate task-related caches after successful creation
559
+ // Note: Task cache would need to be implemented separately if needed
560
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task created successfully', {
561
+ method: 'createTask',
562
+ taskId: response.data.id,
563
+ taskName: response.data.name
564
+ });
565
+ return response.data;
566
+ }
567
+ catch (error) {
568
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create task', {
569
+ method: 'createTask',
570
+ error: getErrorMessage(error),
571
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
572
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
573
+ fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
574
+ });
575
+ throw this.formatApiError(error, 'create task');
576
+ }
577
+ }
578
+ async updateTask(taskId, updates) {
579
+ try {
580
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Updating task in Motion API', {
581
+ method: 'updateTask',
582
+ taskId,
583
+ updates: Object.keys(updates)
584
+ });
585
+ // Create minimal payload by removing empty/null values to avoid validation errors
586
+ const minimalUpdates = (0, constants_1.createMinimalPayload)(updates);
587
+ const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}`, minimalUpdates));
588
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task updated successfully', {
589
+ method: 'updateTask',
590
+ taskId,
591
+ taskName: response.data.name
592
+ });
593
+ return response.data;
594
+ }
595
+ catch (error) {
596
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to update task', {
597
+ method: 'updateTask',
598
+ taskId,
599
+ error: getErrorMessage(error),
600
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
601
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
602
+ });
603
+ throw this.formatApiError(error, 'update task');
604
+ }
605
+ }
606
+ async deleteTask(taskId) {
607
+ try {
608
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting task from Motion API', {
609
+ method: 'deleteTask',
610
+ taskId
611
+ });
612
+ await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}`));
613
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task deleted successfully', {
614
+ method: 'deleteTask',
615
+ taskId
616
+ });
617
+ }
618
+ catch (error) {
619
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete task', {
620
+ method: 'deleteTask',
621
+ taskId,
622
+ error: getErrorMessage(error),
623
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
624
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
625
+ });
626
+ throw this.formatApiError(error, 'delete task');
627
+ }
628
+ }
629
+ async moveTask(taskId, targetProjectId, targetWorkspaceId) {
630
+ try {
631
+ if (!targetProjectId && !targetWorkspaceId) {
632
+ throw new Error('Either targetProjectId or targetWorkspaceId must be provided');
633
+ }
634
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Moving task in Motion API', {
635
+ method: 'moveTask',
636
+ taskId,
637
+ targetProjectId,
638
+ targetWorkspaceId
639
+ });
640
+ const moveData = {};
641
+ if (targetProjectId)
642
+ moveData.projectId = targetProjectId;
643
+ if (targetWorkspaceId)
644
+ moveData.workspaceId = targetWorkspaceId;
645
+ const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}/move`, moveData));
646
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task moved successfully', {
647
+ method: 'moveTask',
648
+ taskId,
649
+ targetProjectId,
650
+ targetWorkspaceId
651
+ });
652
+ // TODO: Invalidate task cache for source and destination projects/workspaces when implemented
653
+ return response.data;
654
+ }
655
+ catch (error) {
656
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to move task', {
657
+ method: 'moveTask',
658
+ taskId,
659
+ targetProjectId,
660
+ targetWorkspaceId,
661
+ error: getErrorMessage(error),
662
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
663
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
664
+ });
665
+ throw this.formatApiError(error, 'move task');
666
+ }
667
+ }
668
+ async unassignTask(taskId) {
669
+ try {
670
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Unassigning task in Motion API', {
671
+ method: 'unassignTask',
672
+ taskId
673
+ });
674
+ const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}/unassign`));
675
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task unassigned successfully', {
676
+ method: 'unassignTask',
677
+ taskId
678
+ });
679
+ // TODO: Invalidate task cache for this task and any assignee-related caches when implemented
680
+ return response.data;
681
+ }
682
+ catch (error) {
683
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to unassign task', {
684
+ method: 'unassignTask',
685
+ taskId,
686
+ error: getErrorMessage(error),
687
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
688
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
689
+ });
690
+ throw this.formatApiError(error, 'unassign task');
691
+ }
692
+ }
693
+ async getWorkspaces() {
694
+ return this.workspaceCache.withCache('workspaces', async () => {
695
+ try {
696
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching workspaces from Motion API', {
697
+ method: 'getWorkspaces'
698
+ });
699
+ const response = await this.requestWithRetry(() => this.client.get('/workspaces'));
700
+ // Validate the response structure
701
+ const validatedResponse = this.validateResponse(response.data, motion_1.WorkspacesListResponseSchema, 'getWorkspaces');
702
+ // Extract workspaces array (handle both wrapped and unwrapped responses)
703
+ const workspaces = Array.isArray(validatedResponse)
704
+ ? validatedResponse
705
+ : validatedResponse.workspaces;
706
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Workspaces fetched and cached successfully', {
707
+ method: 'getWorkspaces',
708
+ count: workspaces.length,
709
+ workspaceNames: workspaces.map((w) => w.name)
710
+ });
711
+ return workspaces;
712
+ }
713
+ catch (error) {
714
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch workspaces', {
715
+ method: 'getWorkspaces',
716
+ error: getErrorMessage(error),
717
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
718
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
719
+ });
720
+ throw this.formatApiError(error, 'fetch workspaces');
721
+ }
722
+ });
723
+ }
724
+ async getUsers(workspaceId) {
725
+ const cacheKey = workspaceId ? `users:workspace:${workspaceId}` : 'users:all';
726
+ return this.userCache.withCache(cacheKey, async () => {
727
+ try {
728
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching users from Motion API', {
729
+ method: 'getUsers',
730
+ workspaceId
731
+ });
732
+ const params = new URLSearchParams();
733
+ if (workspaceId) {
734
+ params.append('workspaceId', workspaceId);
735
+ }
736
+ const queryString = params.toString();
737
+ const url = queryString ? `/users?${queryString}` : '/users';
738
+ const response = await this.requestWithRetry(() => this.client.get(url));
739
+ // The Motion API might wrap the users in a 'users' array
740
+ const usersData = response.data?.users || response.data || [];
741
+ const users = Array.isArray(usersData) ? usersData : [];
742
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Users fetched successfully', {
743
+ method: 'getUsers',
744
+ count: users.length,
745
+ workspaceId
746
+ });
747
+ return users;
748
+ }
749
+ catch (error) {
750
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch users', {
751
+ method: 'getUsers',
752
+ error: getErrorMessage(error),
753
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
754
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
755
+ });
756
+ throw this.formatApiError(error, 'fetch users');
757
+ }
758
+ });
759
+ }
760
+ async getCurrentUser() {
761
+ const cacheKey = 'currentUser';
762
+ // Use userCache but with a special single-user wrapper
763
+ const cachedUsers = await this.userCache.withCache(cacheKey, async () => {
764
+ try {
765
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching current user from Motion API', {
766
+ method: 'getCurrentUser'
767
+ });
768
+ const response = await this.requestWithRetry(() => this.client.get('/users/me'));
769
+ const user = response.data;
770
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Current user fetched successfully', {
771
+ method: 'getCurrentUser',
772
+ userId: user.id,
773
+ email: user.email
774
+ });
775
+ return [user]; // Wrap in array for cache compatibility
776
+ }
777
+ catch (error) {
778
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch current user', {
779
+ method: 'getCurrentUser',
780
+ error: getErrorMessage(error),
781
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
782
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
783
+ });
784
+ throw this.formatApiError(error, 'fetch current user');
785
+ }
786
+ });
787
+ return cachedUsers[0]; // Return just the user object
788
+ }
789
+ // Additional methods for intelligent features
790
+ /**
791
+ * Resolves a project identifier (either projectId or projectName) to a MotionProject
792
+ * Searches across all workspaces if not found in the specified workspace
793
+ * @param identifier Object containing either projectId or projectName
794
+ * @param workspaceId Workspace to start searching in
795
+ * @returns Resolved MotionProject with workspace info, or undefined if not found
796
+ */
797
+ async resolveProjectIdentifier(identifier, workspaceId) {
798
+ try {
799
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Resolving project identifier', {
800
+ method: 'resolveProjectIdentifier',
801
+ projectId: identifier.projectId,
802
+ projectName: identifier.projectName,
803
+ workspaceId
804
+ });
805
+ // If projectId is provided, try to get it directly
806
+ if (identifier.projectId) {
807
+ try {
808
+ const project = await this.getProject(identifier.projectId);
809
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project resolved by ID', {
810
+ method: 'resolveProjectIdentifier',
811
+ projectId: identifier.projectId,
812
+ projectName: project.name,
813
+ workspaceId: project.workspaceId
814
+ });
815
+ return project;
816
+ }
817
+ catch (error) {
818
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve project by ID', {
819
+ method: 'resolveProjectIdentifier',
820
+ projectId: identifier.projectId,
821
+ error: getErrorMessage(error)
822
+ });
823
+ // Fall through to projectName resolution if projectId fails
824
+ }
825
+ }
826
+ // If projectName is provided (or projectId failed), resolve by name across workspaces
827
+ if (identifier.projectName) {
828
+ const project = await this.getProjectByName(identifier.projectName, workspaceId);
829
+ if (project) {
830
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project resolved by name across workspaces', {
831
+ method: 'resolveProjectIdentifier',
832
+ projectName: identifier.projectName,
833
+ projectId: project.id,
834
+ foundInWorkspaceId: project.workspaceId
835
+ });
836
+ return project;
837
+ }
838
+ }
839
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve project identifier', {
840
+ method: 'resolveProjectIdentifier',
841
+ projectId: identifier.projectId,
842
+ projectName: identifier.projectName,
843
+ workspaceId
844
+ });
845
+ return undefined;
846
+ }
847
+ catch (error) {
848
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Error resolving project identifier', {
849
+ method: 'resolveProjectIdentifier',
850
+ projectId: identifier.projectId,
851
+ projectName: identifier.projectName,
852
+ error: getErrorMessage(error)
853
+ });
854
+ throw error;
855
+ }
856
+ }
857
+ /**
858
+ * Resolves a user identifier (either userId or userName/email) to a MotionUser
859
+ * Searches across all workspaces if not found in the specified workspace
860
+ * @param identifier Object containing either userId or userName
861
+ * @param workspaceId Workspace to start searching in (optional)
862
+ * @returns Resolved MotionUser with workspace info, or undefined if not found
863
+ */
864
+ async resolveUserIdentifier(identifier, workspaceId) {
865
+ try {
866
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Resolving user identifier', {
867
+ method: 'resolveUserIdentifier',
868
+ userId: identifier.userId,
869
+ userName: identifier.userName,
870
+ workspaceId
871
+ });
872
+ // If userId is provided, search by ID across all workspaces
873
+ if (identifier.userId) {
874
+ const allWorkspaces = await this.getWorkspaces();
875
+ for (const workspace of allWorkspaces) {
876
+ try {
877
+ const users = await this.getUsers(workspace.id);
878
+ const user = users.find(u => u.id === identifier.userId);
879
+ if (user) {
880
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'User resolved by ID', {
881
+ method: 'resolveUserIdentifier',
882
+ userId: identifier.userId,
883
+ userName: user.name,
884
+ foundInWorkspaceId: workspace.id
885
+ });
886
+ return user;
887
+ }
888
+ }
889
+ catch (workspaceError) {
890
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for user by ID', {
891
+ method: 'resolveUserIdentifier',
892
+ userId: identifier.userId,
893
+ workspaceId: workspace.id,
894
+ error: getErrorMessage(workspaceError)
895
+ });
896
+ }
897
+ }
898
+ }
899
+ // If userName is provided, search by name/email across all workspaces
900
+ if (identifier.userName) {
901
+ const allWorkspaces = await this.getWorkspaces();
902
+ const searchTerm = identifier.userName.toLowerCase();
903
+ for (const workspace of allWorkspaces) {
904
+ try {
905
+ const users = await this.getUsers(workspace.id);
906
+ const user = users.find(u => u.name?.toLowerCase().includes(searchTerm) ||
907
+ u.email?.toLowerCase().includes(searchTerm));
908
+ if (user) {
909
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'User resolved by name/email', {
910
+ method: 'resolveUserIdentifier',
911
+ userName: identifier.userName,
912
+ userId: user.id,
913
+ foundInWorkspaceId: workspace.id
914
+ });
915
+ return user;
916
+ }
917
+ }
918
+ catch (workspaceError) {
919
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for user by name', {
920
+ method: 'resolveUserIdentifier',
921
+ userName: identifier.userName,
922
+ workspaceId: workspace.id,
923
+ error: getErrorMessage(workspaceError)
924
+ });
925
+ }
926
+ }
927
+ }
928
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve user identifier', {
929
+ method: 'resolveUserIdentifier',
930
+ userId: identifier.userId,
931
+ userName: identifier.userName,
932
+ workspaceId
933
+ });
934
+ return undefined;
935
+ }
936
+ catch (error) {
937
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Error resolving user identifier', {
938
+ method: 'resolveUserIdentifier',
939
+ userId: identifier.userId,
940
+ userName: identifier.userName,
941
+ error: getErrorMessage(error)
942
+ });
943
+ throw error;
944
+ }
945
+ }
946
+ async getProjectByName(projectName, workspaceId) {
947
+ try {
948
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Finding project by name', {
949
+ method: 'getProjectByName',
950
+ projectName,
951
+ workspaceId
952
+ });
953
+ // First, search in the specified workspace
954
+ const projects = await this.getProjects(workspaceId);
955
+ const project = projects.find(p => p.name === projectName);
956
+ if (project) {
957
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project found by name in specified workspace', {
958
+ method: 'getProjectByName',
959
+ projectName,
960
+ projectId: project.id,
961
+ workspaceId
962
+ });
963
+ return project;
964
+ }
965
+ // If not found in specified workspace, search across all other workspaces
966
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Project not found in specified workspace, searching all workspaces', {
967
+ method: 'getProjectByName',
968
+ projectName,
969
+ specifiedWorkspaceId: workspaceId
970
+ });
971
+ const allWorkspaces = await this.getWorkspaces();
972
+ const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
973
+ for (const workspace of otherWorkspaces) {
974
+ try {
975
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching workspace for project', {
976
+ method: 'getProjectByName',
977
+ projectName,
978
+ searchingWorkspaceId: workspace.id,
979
+ searchingWorkspaceName: workspace.name
980
+ });
981
+ const workspaceProjects = await this.getProjects(workspace.id);
982
+ const foundProject = workspaceProjects.find(p => p.name === projectName);
983
+ if (foundProject) {
984
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project found by name in different workspace', {
985
+ method: 'getProjectByName',
986
+ projectName,
987
+ projectId: foundProject.id,
988
+ foundInWorkspaceId: workspace.id,
989
+ foundInWorkspaceName: workspace.name,
990
+ originalWorkspaceId: workspaceId
991
+ });
992
+ return foundProject;
993
+ }
994
+ }
995
+ catch (workspaceError) {
996
+ // Log error but continue searching other workspaces
997
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for project', {
998
+ method: 'getProjectByName',
999
+ projectName,
1000
+ workspaceId: workspace.id,
1001
+ workspaceName: workspace.name,
1002
+ error: getErrorMessage(workspaceError)
1003
+ });
1004
+ }
1005
+ }
1006
+ // Project not found in any workspace
1007
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Project not found by name in any workspace', {
1008
+ method: 'getProjectByName',
1009
+ projectName,
1010
+ searchedWorkspaces: [workspaceId, ...otherWorkspaces.map(w => w.id)],
1011
+ totalWorkspacesSearched: allWorkspaces.length
1012
+ });
1013
+ return undefined;
1014
+ }
1015
+ catch (error) {
1016
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to find project by name', {
1017
+ method: 'getProjectByName',
1018
+ projectName,
1019
+ error: getErrorMessage(error)
1020
+ });
1021
+ throw error;
1022
+ }
1023
+ }
1024
+ async searchTasks(query, workspaceId, limit) {
1025
+ try {
1026
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching tasks', {
1027
+ method: 'searchTasks',
1028
+ query,
1029
+ workspaceId,
1030
+ limit
1031
+ });
1032
+ // Apply search limit to prevent resource exhaustion
1033
+ const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1034
+ const lowerQuery = query.toLowerCase();
1035
+ let allMatchingTasks = [];
1036
+ // First, search in the specified workspace
1037
+ const primaryTasks = await this.getTasks(workspaceId, undefined, constants_1.LIMITS.MAX_PAGES, effectiveLimit);
1038
+ const primaryMatches = primaryTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
1039
+ task.description?.toLowerCase().includes(lowerQuery));
1040
+ allMatchingTasks.push(...primaryMatches);
1041
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
1042
+ method: 'searchTasks',
1043
+ query,
1044
+ primaryWorkspaceId: workspaceId,
1045
+ primaryMatches: primaryMatches.length
1046
+ });
1047
+ // If we haven't reached the limit, search other workspaces
1048
+ if (allMatchingTasks.length < effectiveLimit) {
1049
+ try {
1050
+ const allWorkspaces = await this.getWorkspaces();
1051
+ const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
1052
+ for (const workspace of otherWorkspaces) {
1053
+ if (allMatchingTasks.length >= effectiveLimit)
1054
+ break;
1055
+ try {
1056
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for tasks', {
1057
+ method: 'searchTasks',
1058
+ query,
1059
+ searchingWorkspaceId: workspace.id,
1060
+ searchingWorkspaceName: workspace.name
1061
+ });
1062
+ const workspaceTasks = await this.getTasks(workspace.id, undefined, constants_1.LIMITS.MAX_PAGES, effectiveLimit);
1063
+ const workspaceMatches = workspaceTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
1064
+ task.description?.toLowerCase().includes(lowerQuery));
1065
+ allMatchingTasks.push(...workspaceMatches);
1066
+ if (workspaceMatches.length > 0) {
1067
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
1068
+ method: 'searchTasks',
1069
+ query,
1070
+ workspaceId: workspace.id,
1071
+ workspaceName: workspace.name,
1072
+ matches: workspaceMatches.length
1073
+ });
1074
+ }
1075
+ }
1076
+ catch (workspaceError) {
1077
+ // Log error but continue searching other workspaces
1078
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for tasks', {
1079
+ method: 'searchTasks',
1080
+ query,
1081
+ workspaceId: workspace.id,
1082
+ workspaceName: workspace.name,
1083
+ error: getErrorMessage(workspaceError)
1084
+ });
1085
+ }
1086
+ }
1087
+ }
1088
+ catch (workspaceListError) {
1089
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to get workspace list for cross-workspace search', {
1090
+ method: 'searchTasks',
1091
+ query,
1092
+ error: getErrorMessage(workspaceListError)
1093
+ });
1094
+ }
1095
+ }
1096
+ // Apply final limit and return results
1097
+ const finalResults = allMatchingTasks.slice(0, effectiveLimit);
1098
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task search completed across all workspaces', {
1099
+ method: 'searchTasks',
1100
+ query,
1101
+ totalMatches: allMatchingTasks.length,
1102
+ returnedResults: finalResults.length,
1103
+ limit: effectiveLimit,
1104
+ crossWorkspaceSearch: allMatchingTasks.length > primaryMatches.length
1105
+ });
1106
+ return finalResults;
1107
+ }
1108
+ catch (error) {
1109
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search tasks', {
1110
+ method: 'searchTasks',
1111
+ query,
1112
+ error: getErrorMessage(error)
1113
+ });
1114
+ throw error;
1115
+ }
1116
+ }
1117
+ async searchProjects(query, workspaceId, limit) {
1118
+ try {
1119
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching projects', {
1120
+ method: 'searchProjects',
1121
+ query,
1122
+ workspaceId,
1123
+ limit
1124
+ });
1125
+ // Apply search limit to prevent resource exhaustion
1126
+ const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1127
+ const lowerQuery = query.toLowerCase();
1128
+ let allMatchingProjects = [];
1129
+ // First, search in the specified workspace
1130
+ const primaryProjects = await this.getProjects(workspaceId, constants_1.LIMITS.MAX_PAGES);
1131
+ const primaryMatches = primaryProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
1132
+ project.description?.toLowerCase().includes(lowerQuery));
1133
+ allMatchingProjects.push(...primaryMatches);
1134
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
1135
+ method: 'searchProjects',
1136
+ query,
1137
+ primaryWorkspaceId: workspaceId,
1138
+ primaryMatches: primaryMatches.length
1139
+ });
1140
+ // If we haven't reached the limit, search other workspaces
1141
+ if (allMatchingProjects.length < effectiveLimit) {
1142
+ try {
1143
+ const allWorkspaces = await this.getWorkspaces();
1144
+ const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
1145
+ for (const workspace of otherWorkspaces) {
1146
+ if (allMatchingProjects.length >= effectiveLimit)
1147
+ break;
1148
+ try {
1149
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for projects', {
1150
+ method: 'searchProjects',
1151
+ query,
1152
+ searchingWorkspaceId: workspace.id,
1153
+ searchingWorkspaceName: workspace.name
1154
+ });
1155
+ const workspaceProjects = await this.getProjects(workspace.id, constants_1.LIMITS.MAX_PAGES);
1156
+ const workspaceMatches = workspaceProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
1157
+ project.description?.toLowerCase().includes(lowerQuery));
1158
+ allMatchingProjects.push(...workspaceMatches);
1159
+ if (workspaceMatches.length > 0) {
1160
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
1161
+ method: 'searchProjects',
1162
+ query,
1163
+ workspaceId: workspace.id,
1164
+ workspaceName: workspace.name,
1165
+ matches: workspaceMatches.length
1166
+ });
1167
+ }
1168
+ }
1169
+ catch (workspaceError) {
1170
+ // Log error but continue searching other workspaces
1171
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for projects', {
1172
+ method: 'searchProjects',
1173
+ query,
1174
+ workspaceId: workspace.id,
1175
+ workspaceName: workspace.name,
1176
+ error: getErrorMessage(workspaceError)
1177
+ });
1178
+ }
1179
+ }
1180
+ }
1181
+ catch (workspaceListError) {
1182
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to get workspace list for cross-workspace search', {
1183
+ method: 'searchProjects',
1184
+ query,
1185
+ error: getErrorMessage(workspaceListError)
1186
+ });
1187
+ }
1188
+ }
1189
+ // Apply final limit and return results
1190
+ const finalResults = allMatchingProjects.slice(0, effectiveLimit);
1191
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project search completed across all workspaces', {
1192
+ method: 'searchProjects',
1193
+ query,
1194
+ totalMatches: allMatchingProjects.length,
1195
+ returnedResults: finalResults.length,
1196
+ limit: effectiveLimit,
1197
+ crossWorkspaceSearch: allMatchingProjects.length > primaryMatches.length
1198
+ });
1199
+ return finalResults;
1200
+ }
1201
+ catch (error) {
1202
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search projects', {
1203
+ method: 'searchProjects',
1204
+ query,
1205
+ error: getErrorMessage(error)
1206
+ });
1207
+ throw error;
1208
+ }
1209
+ }
1210
+ /**
1211
+ * Get comments for a task with proper pagination support
1212
+ * @param taskId Task ID to get comments for
1213
+ * @param cursor Optional cursor for pagination
1214
+ * @returns Paginated response with comments and metadata
1215
+ */
1216
+ async getComments(taskId, cursor) {
1217
+ const cacheParams = { taskId, cursor: cursor || null };
1218
+ const cacheKey = `comments:${JSON.stringify(cacheParams)}`;
1219
+ return this.commentCache.withCache(cacheKey, async () => {
1220
+ try {
1221
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching comments from Motion API', {
1222
+ method: 'getComments',
1223
+ taskId,
1224
+ cursor
1225
+ });
1226
+ const params = new URLSearchParams({ taskId });
1227
+ if (cursor)
1228
+ params.append('cursor', cursor);
1229
+ const response = await this.requestWithRetry(() => this.client.get(`/comments?${params.toString()}`));
1230
+ // Use new response wrapper for consistent handling
1231
+ const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'comments');
1232
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Comments fetched successfully', {
1233
+ method: 'getComments',
1234
+ count: unwrapped.data.length,
1235
+ hasMore: !!unwrapped.meta?.nextCursor,
1236
+ taskId
1237
+ });
1238
+ // Return in our standard paginated format
1239
+ return {
1240
+ data: unwrapped.data,
1241
+ meta: {
1242
+ nextCursor: unwrapped.meta?.nextCursor,
1243
+ pageSize: unwrapped.meta?.pageSize || unwrapped.data.length
1244
+ }
1245
+ };
1246
+ }
1247
+ catch (error) {
1248
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch comments', {
1249
+ method: 'getComments',
1250
+ error: getErrorMessage(error),
1251
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1252
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1253
+ taskId,
1254
+ cursor
1255
+ });
1256
+ throw this.formatApiError(error, 'fetch comments');
1257
+ }
1258
+ });
1259
+ }
1260
+ async createComment(commentData) {
1261
+ try {
1262
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating comment in Motion API', {
1263
+ method: 'createComment',
1264
+ taskId: commentData.taskId,
1265
+ contentLength: commentData.content?.length || 0
1266
+ });
1267
+ // Create minimal payload by removing empty/null values to avoid validation errors
1268
+ const minimalPayload = (0, constants_1.createMinimalPayload)(commentData);
1269
+ const response = await this.requestWithRetry(() => this.client.post('/comments', minimalPayload));
1270
+ // Invalidate cache after successful creation
1271
+ const cacheKey = `comments:${JSON.stringify({ taskId: commentData.taskId, cursor: null })}`;
1272
+ this.commentCache.invalidate(cacheKey);
1273
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Comment created successfully', {
1274
+ method: 'createComment',
1275
+ commentId: response.data?.id,
1276
+ taskId: commentData.taskId
1277
+ });
1278
+ return response.data;
1279
+ }
1280
+ catch (error) {
1281
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create comment', {
1282
+ method: 'createComment',
1283
+ error: getErrorMessage(error),
1284
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1285
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1286
+ taskId: commentData?.taskId
1287
+ });
1288
+ throw this.formatApiError(error, 'create comment');
1289
+ }
1290
+ }
1291
+ /**
1292
+ * Fetch custom fields from Motion API
1293
+ * @param workspaceId - Required workspace ID to get custom fields for
1294
+ * @returns Array of custom fields
1295
+ */
1296
+ async getCustomFields(workspaceId) {
1297
+ const cacheKey = `custom-fields:${workspaceId}`;
1298
+ return this.customFieldCache.withCache(cacheKey, async () => {
1299
+ try {
1300
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching custom fields from Motion API', {
1301
+ method: 'getCustomFields',
1302
+ workspaceId
1303
+ });
1304
+ const url = `/beta/workspaces/${workspaceId}/custom-fields`;
1305
+ const response = await this.requestWithRetry(() => this.client.get(url));
1306
+ // Beta API returns direct array, not wrapped
1307
+ const fieldsArray = response.data || [];
1308
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom fields fetched successfully', {
1309
+ method: 'getCustomFields',
1310
+ count: fieldsArray.length,
1311
+ workspaceId
1312
+ });
1313
+ return fieldsArray;
1314
+ }
1315
+ catch (error) {
1316
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch custom fields', {
1317
+ method: 'getCustomFields',
1318
+ error: getErrorMessage(error),
1319
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1320
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1321
+ workspaceId
1322
+ });
1323
+ throw this.formatApiError(error, 'fetch custom fields');
1324
+ }
1325
+ });
1326
+ }
1327
+ /**
1328
+ * Create a new custom field
1329
+ * @param workspaceId - Required workspace ID to create custom field in
1330
+ * @param fieldData - Data for creating the custom field
1331
+ * @returns The created custom field
1332
+ */
1333
+ async createCustomField(workspaceId, fieldData) {
1334
+ try {
1335
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating custom field in Motion API', {
1336
+ method: 'createCustomField',
1337
+ name: fieldData.name,
1338
+ field: fieldData.field,
1339
+ workspaceId
1340
+ });
1341
+ // Transform payload to match Motion API expectations
1342
+ // POST API expects 'type' in request, but returns 'field' in response
1343
+ const apiPayload = {
1344
+ name: fieldData.name,
1345
+ type: fieldData.field, // Motion API POST expects 'type' property in request body
1346
+ ...(fieldData.metadata && { metadata: fieldData.metadata })
1347
+ };
1348
+ // Create minimal payload by removing empty/null values to avoid validation errors
1349
+ const minimalPayload = (0, constants_1.createMinimalPayload)(apiPayload);
1350
+ const response = await this.requestWithRetry(() => this.client.post(`/beta/workspaces/${workspaceId}/custom-fields`, minimalPayload));
1351
+ // Invalidate cache after successful creation
1352
+ this.customFieldCache.invalidate(`custom-fields:${workspaceId}`);
1353
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field created successfully', {
1354
+ method: 'createCustomField',
1355
+ fieldId: response.data?.id,
1356
+ name: fieldData.name,
1357
+ workspaceId
1358
+ });
1359
+ return response.data;
1360
+ }
1361
+ catch (error) {
1362
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create custom field', {
1363
+ method: 'createCustomField',
1364
+ error: getErrorMessage(error),
1365
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1366
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1367
+ fieldName: fieldData?.name,
1368
+ workspaceId
1369
+ });
1370
+ throw this.formatApiError(error, 'create custom field');
1371
+ }
1372
+ }
1373
+ /**
1374
+ * Delete a custom field
1375
+ * @param workspaceId - Required workspace ID containing the custom field
1376
+ * @param fieldId - ID of the custom field to delete
1377
+ * @returns Success indicator
1378
+ */
1379
+ async deleteCustomField(workspaceId, fieldId) {
1380
+ try {
1381
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting custom field from Motion API', {
1382
+ method: 'deleteCustomField',
1383
+ fieldId,
1384
+ workspaceId
1385
+ });
1386
+ await this.requestWithRetry(() => this.client.delete(`/beta/workspaces/${workspaceId}/custom-fields/${fieldId}`));
1387
+ // Invalidate cache after successful deletion
1388
+ this.customFieldCache.invalidate(`custom-fields:${workspaceId}`);
1389
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field deleted successfully', {
1390
+ method: 'deleteCustomField',
1391
+ fieldId,
1392
+ workspaceId
1393
+ });
1394
+ return { success: true };
1395
+ }
1396
+ catch (error) {
1397
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete custom field', {
1398
+ method: 'deleteCustomField',
1399
+ error: getErrorMessage(error),
1400
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1401
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1402
+ fieldId,
1403
+ workspaceId
1404
+ });
1405
+ throw this.formatApiError(error, 'delete custom field');
1406
+ }
1407
+ }
1408
+ /**
1409
+ * Add a custom field to a project
1410
+ * @param projectId - ID of the project
1411
+ * @param fieldId - ID of the custom field
1412
+ * @param value - Optional value for the field
1413
+ * @returns Updated project data
1414
+ */
1415
+ async addCustomFieldToProject(projectId, fieldId, value) {
1416
+ try {
1417
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Adding custom field to project', {
1418
+ method: 'addCustomFieldToProject',
1419
+ projectId,
1420
+ fieldId,
1421
+ hasValue: value !== undefined
1422
+ });
1423
+ const requestData = {
1424
+ fieldId,
1425
+ ...(value !== undefined && { value })
1426
+ };
1427
+ const response = await this.requestWithRetry(() => this.client.post(`/projects/${projectId}/custom-fields`, requestData));
1428
+ // Invalidate project cache for specific workspace if available
1429
+ if (response.data?.workspaceId) {
1430
+ this.projectCache.invalidate(`projects:workspace:${response.data.workspaceId}`);
1431
+ }
1432
+ else {
1433
+ // Fallback to broader invalidation if workspace unknown
1434
+ this.projectCache.invalidate(`projects:`);
1435
+ }
1436
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to project successfully', {
1437
+ method: 'addCustomFieldToProject',
1438
+ projectId,
1439
+ fieldId
1440
+ });
1441
+ return response.data;
1442
+ }
1443
+ catch (error) {
1444
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to add custom field to project', {
1445
+ method: 'addCustomFieldToProject',
1446
+ error: getErrorMessage(error),
1447
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1448
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1449
+ projectId,
1450
+ fieldId
1451
+ });
1452
+ throw this.formatApiError(error, 'add custom field to project');
1453
+ }
1454
+ }
1455
+ /**
1456
+ * Remove a custom field from a project
1457
+ * @param projectId - ID of the project
1458
+ * @param fieldId - ID of the custom field
1459
+ * @returns Success indicator
1460
+ */
1461
+ async removeCustomFieldFromProject(projectId, fieldId) {
1462
+ try {
1463
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Removing custom field from project', {
1464
+ method: 'removeCustomFieldFromProject',
1465
+ projectId,
1466
+ fieldId
1467
+ });
1468
+ await this.requestWithRetry(() => this.client.delete(`/projects/${projectId}/custom-fields/${fieldId}`));
1469
+ // TODO: Invalidate project cache for specific workspace when project data is available
1470
+ // For now, invalidate all project caches
1471
+ this.projectCache.invalidate(`projects:`);
1472
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from project successfully', {
1473
+ method: 'removeCustomFieldFromProject',
1474
+ projectId,
1475
+ fieldId
1476
+ });
1477
+ return { success: true };
1478
+ }
1479
+ catch (error) {
1480
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to remove custom field from project', {
1481
+ method: 'removeCustomFieldFromProject',
1482
+ error: getErrorMessage(error),
1483
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1484
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1485
+ projectId,
1486
+ fieldId
1487
+ });
1488
+ throw this.formatApiError(error, 'remove custom field from project');
1489
+ }
1490
+ }
1491
+ /**
1492
+ * Add a custom field to a task
1493
+ * @param taskId - ID of the task
1494
+ * @param fieldId - ID of the custom field
1495
+ * @param value - Optional value for the field
1496
+ * @returns Updated task data
1497
+ */
1498
+ async addCustomFieldToTask(taskId, fieldId, value) {
1499
+ try {
1500
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Adding custom field to task', {
1501
+ method: 'addCustomFieldToTask',
1502
+ taskId,
1503
+ fieldId,
1504
+ hasValue: value !== undefined
1505
+ });
1506
+ const requestData = {
1507
+ fieldId,
1508
+ ...(value !== undefined && { value })
1509
+ };
1510
+ const response = await this.requestWithRetry(() => this.client.post(`/tasks/${taskId}/custom-fields`, requestData));
1511
+ // TODO: Invalidate task cache when implemented
1512
+ // if (response.data?.workspaceId) {
1513
+ // this.taskCache.invalidate(`tasks:workspace:${response.data.workspaceId}`);
1514
+ // }
1515
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to task successfully', {
1516
+ method: 'addCustomFieldToTask',
1517
+ taskId,
1518
+ fieldId
1519
+ });
1520
+ return response.data;
1521
+ }
1522
+ catch (error) {
1523
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to add custom field to task', {
1524
+ method: 'addCustomFieldToTask',
1525
+ error: getErrorMessage(error),
1526
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1527
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1528
+ taskId,
1529
+ fieldId
1530
+ });
1531
+ throw this.formatApiError(error, 'add custom field to task');
1532
+ }
1533
+ }
1534
+ /**
1535
+ * Remove a custom field from a task
1536
+ * @param taskId - ID of the task
1537
+ * @param fieldId - ID of the custom field
1538
+ * @returns Success indicator
1539
+ */
1540
+ async removeCustomFieldFromTask(taskId, fieldId) {
1541
+ try {
1542
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Removing custom field from task', {
1543
+ method: 'removeCustomFieldFromTask',
1544
+ taskId,
1545
+ fieldId
1546
+ });
1547
+ await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}/custom-fields/${fieldId}`));
1548
+ // TODO: Invalidate task cache when implemented
1549
+ // this.taskCache.invalidate(`tasks:`);
1550
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from task successfully', {
1551
+ method: 'removeCustomFieldFromTask',
1552
+ taskId,
1553
+ fieldId
1554
+ });
1555
+ return { success: true };
1556
+ }
1557
+ catch (error) {
1558
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to remove custom field from task', {
1559
+ method: 'removeCustomFieldFromTask',
1560
+ error: getErrorMessage(error),
1561
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1562
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1563
+ taskId,
1564
+ fieldId
1565
+ });
1566
+ throw this.formatApiError(error, 'remove custom field from task');
1567
+ }
1568
+ }
1569
+ /**
1570
+ * Fetch recurring tasks from Motion API with automatic pagination
1571
+ * @param workspaceId - Optional workspace ID to filter recurring tasks
1572
+ * @param maxPages - Maximum number of pages to fetch (default: 10)
1573
+ * @returns Array of recurring tasks from all pages
1574
+ */
1575
+ async getRecurringTasks(workspaceId, maxPages = 10) {
1576
+ const cacheKey = workspaceId ? `recurring-tasks:workspace:${workspaceId}` : 'recurring-tasks:all';
1577
+ return this.recurringTaskCache.withCache(cacheKey, async () => {
1578
+ try {
1579
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching recurring tasks from Motion API with pagination', {
1580
+ method: 'getRecurringTasks',
1581
+ workspaceId,
1582
+ maxPages
1583
+ });
1584
+ // Create a fetch function for pagination utility
1585
+ const fetchPage = async (cursor) => {
1586
+ const params = new URLSearchParams();
1587
+ if (workspaceId)
1588
+ params.append('workspaceId', workspaceId);
1589
+ if (cursor)
1590
+ params.append('cursor', cursor);
1591
+ const queryString = params.toString();
1592
+ const url = queryString ? `/recurring-tasks?${queryString}` : '/recurring-tasks';
1593
+ return this.requestWithRetry(() => this.client.get(url));
1594
+ };
1595
+ // Use pagination utility to fetch all pages
1596
+ const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'recurring-tasks', {
1597
+ maxPages,
1598
+ logProgress: true
1599
+ });
1600
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring tasks fetched successfully with pagination', {
1601
+ method: 'getRecurringTasks',
1602
+ totalCount: paginatedResult.totalFetched,
1603
+ pagesProcessed: Math.ceil(paginatedResult.totalFetched / 50), // Assuming ~50 items per page
1604
+ hasMore: paginatedResult.hasMore,
1605
+ workspaceId
1606
+ });
1607
+ return paginatedResult.items;
1608
+ }
1609
+ catch (error) {
1610
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch recurring tasks', {
1611
+ method: 'getRecurringTasks',
1612
+ error: getErrorMessage(error),
1613
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1614
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1615
+ workspaceId
1616
+ });
1617
+ throw this.formatApiError(error, 'fetch recurring tasks');
1618
+ }
1619
+ });
1620
+ }
1621
+ /**
1622
+ * Create a new recurring task
1623
+ * @param taskData - Data for creating the recurring task
1624
+ * @returns The created recurring task
1625
+ */
1626
+ async createRecurringTask(taskData) {
1627
+ try {
1628
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating recurring task in Motion API', {
1629
+ method: 'createRecurringTask',
1630
+ name: taskData.name,
1631
+ assigneeId: taskData.assigneeId,
1632
+ frequency: taskData.frequency.type,
1633
+ workspaceId: taskData.workspaceId
1634
+ });
1635
+ // Create minimal payload by removing empty/null values to avoid validation errors
1636
+ const minimalPayload = (0, constants_1.createMinimalPayload)(taskData);
1637
+ const response = await this.requestWithRetry(() => this.client.post('/recurring-tasks', minimalPayload));
1638
+ // Invalidate cache after successful creation
1639
+ this.recurringTaskCache.invalidate('recurring-tasks:');
1640
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring task created successfully', {
1641
+ method: 'createRecurringTask',
1642
+ taskId: response.data?.id,
1643
+ name: taskData.name
1644
+ });
1645
+ return response.data;
1646
+ }
1647
+ catch (error) {
1648
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create recurring task', {
1649
+ method: 'createRecurringTask',
1650
+ error: getErrorMessage(error),
1651
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1652
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1653
+ taskName: taskData?.name
1654
+ });
1655
+ throw this.formatApiError(error, 'create recurring task');
1656
+ }
1657
+ }
1658
+ /**
1659
+ * Delete a recurring task
1660
+ * @param recurringTaskId - ID of the recurring task to delete
1661
+ * @returns Success indicator
1662
+ */
1663
+ async deleteRecurringTask(recurringTaskId) {
1664
+ try {
1665
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting recurring task from Motion API', {
1666
+ method: 'deleteRecurringTask',
1667
+ recurringTaskId
1668
+ });
1669
+ await this.requestWithRetry(() => this.client.delete(`/recurring-tasks/${recurringTaskId}`));
1670
+ // Invalidate cache after successful deletion
1671
+ this.recurringTaskCache.invalidate('recurring-tasks:');
1672
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring task deleted successfully', {
1673
+ method: 'deleteRecurringTask',
1674
+ recurringTaskId
1675
+ });
1676
+ return { success: true };
1677
+ }
1678
+ catch (error) {
1679
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete recurring task', {
1680
+ method: 'deleteRecurringTask',
1681
+ error: getErrorMessage(error),
1682
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1683
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1684
+ recurringTaskId
1685
+ });
1686
+ throw this.formatApiError(error, 'delete recurring task');
1687
+ }
1688
+ }
1689
+ /**
1690
+ * Get available schedule names for auto-scheduling
1691
+ * @param workspaceId - Optional workspace ID to filter schedules (currently unused by Motion API)
1692
+ * @returns Array of schedule names
1693
+ */
1694
+ async getAvailableScheduleNames(workspaceId) {
1695
+ try {
1696
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching available schedule names', {
1697
+ method: 'getAvailableScheduleNames',
1698
+ workspaceId
1699
+ });
1700
+ // Fetch all schedules without filters to get available schedule templates
1701
+ const schedules = await this.getSchedules();
1702
+ const scheduleNames = schedules.map(schedule => schedule.name).filter(Boolean);
1703
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Available schedule names fetched successfully', {
1704
+ method: 'getAvailableScheduleNames',
1705
+ count: scheduleNames.length,
1706
+ scheduleNames
1707
+ });
1708
+ return scheduleNames;
1709
+ }
1710
+ catch (error) {
1711
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch available schedule names', {
1712
+ method: 'getAvailableScheduleNames',
1713
+ error: getErrorMessage(error),
1714
+ workspaceId
1715
+ });
1716
+ throw this.formatApiError(error, 'fetch available schedule names');
1717
+ }
1718
+ }
1719
+ /**
1720
+ * Fetch schedules from Motion API
1721
+ * @param userId - Optional user ID to filter schedules
1722
+ * @param startDate - Optional start date (ISO 8601) to filter schedules
1723
+ * @param endDate - Optional end date (ISO 8601) to filter schedules
1724
+ * @returns Array of schedules
1725
+ */
1726
+ async getSchedules(userId, startDate, endDate) {
1727
+ // Use JSON.stringify for deterministic cache key generation to avoid collisions
1728
+ const cacheParams = { userId: userId || null, startDate: startDate || null, endDate: endDate || null };
1729
+ const cacheKey = `schedules:${JSON.stringify(cacheParams)}`;
1730
+ return this.scheduleCache.withCache(cacheKey, async () => {
1731
+ try {
1732
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching schedules from Motion API', {
1733
+ method: 'getSchedules',
1734
+ userId,
1735
+ startDate,
1736
+ endDate
1737
+ });
1738
+ const params = new URLSearchParams();
1739
+ if (userId)
1740
+ params.append('userId', userId);
1741
+ if (startDate)
1742
+ params.append('startDate', startDate);
1743
+ if (endDate)
1744
+ params.append('endDate', endDate);
1745
+ const queryString = params.toString();
1746
+ const url = queryString ? `/schedules?${queryString}` : '/schedules';
1747
+ const response = await this.requestWithRetry(() => this.client.get(url));
1748
+ // Validate response against schema
1749
+ const validatedResponse = this.validateResponse(response.data, motion_1.SchedulesListResponseSchema, 'getSchedules');
1750
+ // Handle both wrapped and unwrapped responses
1751
+ const schedules = Array.isArray(validatedResponse)
1752
+ ? validatedResponse
1753
+ : validatedResponse.schedules || [];
1754
+ const schedulesArray = Array.isArray(schedules) ? schedules : [];
1755
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Schedules fetched successfully', {
1756
+ method: 'getSchedules',
1757
+ count: schedulesArray.length,
1758
+ userId,
1759
+ startDate,
1760
+ endDate
1761
+ });
1762
+ return schedulesArray;
1763
+ }
1764
+ catch (error) {
1765
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch schedules', {
1766
+ method: 'getSchedules',
1767
+ error: getErrorMessage(error),
1768
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1769
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1770
+ userId,
1771
+ startDate,
1772
+ endDate
1773
+ });
1774
+ throw this.formatApiError(error, 'fetch schedules');
1775
+ }
1776
+ });
1777
+ }
1778
+ /**
1779
+ * Retrieves available workflow statuses from Motion
1780
+ * @param workspaceId - Optional workspace ID to filter statuses
1781
+ * @returns Promise resolving to array of Motion statuses
1782
+ * @throws {Error} If the API request fails
1783
+ */
1784
+ async getStatuses(workspaceId) {
1785
+ // Use workspace ID for cache key, or 'all' if not specified
1786
+ const cacheKey = workspaceId ? `statuses:workspace:${workspaceId}` : 'statuses:all';
1787
+ return this.statusCache.withCache(cacheKey, async () => {
1788
+ try {
1789
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching statuses from Motion API', {
1790
+ method: 'getStatuses',
1791
+ workspaceId
1792
+ });
1793
+ const params = new URLSearchParams();
1794
+ if (workspaceId)
1795
+ params.append('workspaceId', workspaceId);
1796
+ const queryString = params.toString();
1797
+ const url = queryString ? `/statuses?${queryString}` : '/statuses';
1798
+ const response = await this.requestWithRetry(() => this.client.get(url));
1799
+ // Validate response against schema
1800
+ const validatedResponse = this.validateResponse(response.data, motion_1.StatusesListResponseSchema, 'statuses');
1801
+ // Extract statuses from validated response
1802
+ const statuses = Array.isArray(validatedResponse)
1803
+ ? validatedResponse
1804
+ : ('statuses' in validatedResponse && Array.isArray(validatedResponse.statuses))
1805
+ ? validatedResponse.statuses
1806
+ : [];
1807
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Statuses fetched successfully', {
1808
+ method: 'getStatuses',
1809
+ count: statuses.length,
1810
+ workspaceId
1811
+ });
1812
+ return statuses;
1813
+ }
1814
+ catch (error) {
1815
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch statuses', {
1816
+ method: 'getStatuses',
1817
+ error: getErrorMessage(error),
1818
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1819
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1820
+ workspaceId
1821
+ });
1822
+ throw this.formatApiError(error, 'fetch statuses');
1823
+ }
1824
+ });
1825
+ }
1826
+ /**
1827
+ * Get all uncompleted tasks across all workspaces and projects
1828
+ * Filters tasks where status.isResolvedStatus is false or undefined
1829
+ */
1830
+ async getAllUncompletedTasks(limit) {
1831
+ try {
1832
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching all uncompleted tasks across workspaces', {
1833
+ method: 'getAllUncompletedTasks',
1834
+ limit
1835
+ });
1836
+ // Apply limit to prevent resource exhaustion
1837
+ const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1838
+ const allUncompletedTasks = [];
1839
+ try {
1840
+ // Get all workspaces
1841
+ const workspaces = await this.getWorkspaces();
1842
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching for uncompleted tasks across workspaces', {
1843
+ method: 'getAllUncompletedTasks',
1844
+ totalWorkspaces: workspaces.length
1845
+ });
1846
+ // Fetch tasks from each workspace
1847
+ for (const workspace of workspaces) {
1848
+ if (allUncompletedTasks.length >= effectiveLimit) {
1849
+ break; // Stop if we've reached the limit
1850
+ }
1851
+ try {
1852
+ // Get all tasks from this workspace (all projects)
1853
+ const workspaceTasks = await this.getTasks(workspace.id, undefined, constants_1.LIMITS.MAX_PAGES, effectiveLimit);
1854
+ // Filter for uncompleted tasks
1855
+ const uncompletedTasks = workspaceTasks.filter(task => {
1856
+ // Task is uncompleted if status is missing or isResolvedStatus is false
1857
+ if (!task.status)
1858
+ return true; // No status = not resolved
1859
+ if (typeof task.status === 'string')
1860
+ return true; // Simple string status = assume not resolved
1861
+ return !task.status.isResolvedStatus; // Object status with isResolvedStatus false
1862
+ });
1863
+ allUncompletedTasks.push(...uncompletedTasks);
1864
+ if (uncompletedTasks.length > 0) {
1865
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found uncompleted tasks in workspace', {
1866
+ method: 'getAllUncompletedTasks',
1867
+ workspaceId: workspace.id,
1868
+ workspaceName: workspace.name,
1869
+ uncompletedTasks: uncompletedTasks.length,
1870
+ totalTasks: workspaceTasks.length
1871
+ });
1872
+ }
1873
+ }
1874
+ catch (workspaceError) {
1875
+ // Log error but continue with other workspaces
1876
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to fetch tasks from workspace', {
1877
+ method: 'getAllUncompletedTasks',
1878
+ workspaceId: workspace.id,
1879
+ workspaceName: workspace.name,
1880
+ error: getErrorMessage(workspaceError)
1881
+ });
1882
+ }
1883
+ }
1884
+ }
1885
+ catch (workspaceListError) {
1886
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to get workspace list', {
1887
+ method: 'getAllUncompletedTasks',
1888
+ error: getErrorMessage(workspaceListError)
1889
+ });
1890
+ throw workspaceListError;
1891
+ }
1892
+ // Apply final limit and return results
1893
+ const finalResults = allUncompletedTasks.slice(0, effectiveLimit);
1894
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'All uncompleted tasks fetched successfully', {
1895
+ method: 'getAllUncompletedTasks',
1896
+ totalFound: allUncompletedTasks.length,
1897
+ returned: finalResults.length,
1898
+ limit: effectiveLimit
1899
+ });
1900
+ return finalResults;
1901
+ }
1902
+ catch (error) {
1903
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch all uncompleted tasks', {
1904
+ method: 'getAllUncompletedTasks',
1905
+ error: getErrorMessage(error)
1906
+ });
1907
+ throw error;
1908
+ }
1909
+ }
1910
+ }
1911
+ exports.MotionApiService = MotionApiService;
1912
+ //# sourceMappingURL=motionApi.js.map