motionmcp 1.0.2 → 2.1.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 +241 -450
  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 +324 -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.d.ts +15 -0
  60. package/dist/mcp-server.d.ts.map +1 -0
  61. package/dist/mcp-server.js +145 -0
  62. package/dist/mcp-server.js.map +1 -0
  63. package/dist/schemas/motion.d.ts +4971 -0
  64. package/dist/schemas/motion.d.ts.map +1 -0
  65. package/dist/schemas/motion.js +328 -0
  66. package/dist/schemas/motion.js.map +1 -0
  67. package/dist/services/motionApi.d.ts +199 -0
  68. package/dist/services/motionApi.d.ts.map +1 -0
  69. package/dist/services/motionApi.js +1950 -0
  70. package/dist/services/motionApi.js.map +1 -0
  71. package/dist/tools/ToolConfigurator.d.ts +19 -0
  72. package/dist/tools/ToolConfigurator.d.ts.map +1 -0
  73. package/dist/tools/ToolConfigurator.js +89 -0
  74. package/dist/tools/ToolConfigurator.js.map +1 -0
  75. package/dist/tools/ToolDefinitions.d.ts +25 -0
  76. package/dist/tools/ToolDefinitions.d.ts.map +1 -0
  77. package/dist/tools/ToolDefinitions.js +508 -0
  78. package/dist/tools/ToolDefinitions.js.map +1 -0
  79. package/dist/tools/ToolRegistry.d.ts +16 -0
  80. package/dist/tools/ToolRegistry.d.ts.map +1 -0
  81. package/dist/tools/ToolRegistry.js +89 -0
  82. package/dist/tools/ToolRegistry.js.map +1 -0
  83. package/dist/tools/index.d.ts +4 -0
  84. package/dist/tools/index.d.ts.map +1 -0
  85. package/dist/tools/index.js +21 -0
  86. package/dist/tools/index.js.map +1 -0
  87. package/dist/types/mcp-tool-args.d.ts +123 -0
  88. package/dist/types/mcp-tool-args.d.ts.map +1 -0
  89. package/dist/types/mcp-tool-args.js +7 -0
  90. package/dist/types/mcp-tool-args.js.map +1 -0
  91. package/dist/types/mcp.d.ts +32 -0
  92. package/dist/types/mcp.d.ts.map +1 -0
  93. package/dist/types/mcp.js +3 -0
  94. package/dist/types/mcp.js.map +1 -0
  95. package/dist/types/motion.d.ts +304 -0
  96. package/dist/types/motion.d.ts.map +1 -0
  97. package/dist/types/motion.js +3 -0
  98. package/dist/types/motion.js.map +1 -0
  99. package/dist/utils/cache.d.ts +25 -0
  100. package/dist/utils/cache.d.ts.map +1 -0
  101. package/dist/utils/cache.js +135 -0
  102. package/dist/utils/cache.js.map +1 -0
  103. package/dist/utils/constants.d.ts +88 -0
  104. package/dist/utils/constants.d.ts.map +1 -0
  105. package/dist/utils/constants.js +188 -0
  106. package/dist/utils/constants.js.map +1 -0
  107. package/dist/utils/errorHandling.d.ts +50 -0
  108. package/dist/utils/errorHandling.d.ts.map +1 -0
  109. package/dist/utils/errorHandling.js +86 -0
  110. package/dist/utils/errorHandling.js.map +1 -0
  111. package/dist/utils/index.d.ts +13 -0
  112. package/dist/utils/index.d.ts.map +1 -0
  113. package/dist/utils/index.js +38 -0
  114. package/dist/utils/index.js.map +1 -0
  115. package/dist/utils/logger.d.ts +13 -0
  116. package/dist/utils/logger.d.ts.map +1 -0
  117. package/dist/utils/logger.js +47 -0
  118. package/dist/utils/logger.js.map +1 -0
  119. package/dist/utils/pagination.d.ts +61 -0
  120. package/dist/utils/pagination.d.ts.map +1 -0
  121. package/dist/utils/pagination.js +168 -0
  122. package/dist/utils/pagination.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 +95 -0
  132. package/dist/utils/responseFormatters.d.ts.map +1 -0
  133. package/dist/utils/responseFormatters.js +342 -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 +138 -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 -179
  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,1950 @@
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);
108
+ this.userCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.USERS);
109
+ this.projectCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.PROJECTS);
110
+ this.singleProjectCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.PROJECTS);
111
+ this.commentCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.COMMENTS);
112
+ this.customFieldCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.CUSTOM_FIELDS);
113
+ this.recurringTaskCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.RECURRING_TASKS);
114
+ this.scheduleCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.SCHEDULES);
115
+ this.statusCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.WORKSPACES); // 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(options) {
435
+ const { workspaceId, projectId, status, assigneeId, priority, dueDate, labels, limit, maxPages = 5 } = options;
436
+ try {
437
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching tasks from Motion API', {
438
+ method: 'getTasks',
439
+ workspaceId,
440
+ projectId,
441
+ status,
442
+ assigneeId,
443
+ priority,
444
+ dueDate,
445
+ labelsCount: labels?.length,
446
+ maxPages
447
+ });
448
+ // Create a fetch function for potential pagination
449
+ const fetchPage = async (cursor) => {
450
+ const params = new URLSearchParams();
451
+ params.append('workspaceId', workspaceId);
452
+ if (projectId) {
453
+ params.append('projectId', projectId);
454
+ }
455
+ if (status) {
456
+ params.append('status', status);
457
+ }
458
+ if (assigneeId) {
459
+ params.append('assigneeId', assigneeId);
460
+ }
461
+ if (priority) {
462
+ params.append('priority', priority);
463
+ }
464
+ if (dueDate) {
465
+ params.append('dueDate', dueDate);
466
+ }
467
+ if (labels && labels.length > 0) {
468
+ for (const label of labels) {
469
+ if (label) {
470
+ params.append('labels', label);
471
+ }
472
+ }
473
+ }
474
+ if (cursor) {
475
+ params.append('cursor', cursor);
476
+ }
477
+ const queryString = params.toString();
478
+ const url = queryString ? `/tasks?${queryString}` : '/tasks';
479
+ return this.requestWithRetry(() => this.client.get(url));
480
+ };
481
+ try {
482
+ // Attempt pagination-aware fetch with new response wrapper
483
+ const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'tasks', {
484
+ maxPages,
485
+ logProgress: false, // Less verbose for tasks
486
+ ...(limit ? { maxItems: limit } : {})
487
+ });
488
+ 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
+ }
494
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully with pagination', {
495
+ method: 'getTasks',
496
+ totalCount: paginatedResult.totalFetched,
497
+ returnedCount: tasks.length,
498
+ hasMore: paginatedResult.hasMore,
499
+ workspaceId,
500
+ projectId,
501
+ limitApplied: limit
502
+ });
503
+ return tasks;
504
+ }
505
+ }
506
+ catch (paginationError) {
507
+ // Fallback to simple fetch if pagination fails
508
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
509
+ method: 'getTasks',
510
+ error: paginationError instanceof Error ? paginationError.message : String(paginationError)
511
+ });
512
+ }
513
+ // Use new response wrapper for single page fallback
514
+ const response = await fetchPage();
515
+ const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'tasks');
516
+ let tasks = unwrapped.data;
517
+ // Apply limit if specified
518
+ if (limit && limit > 0) {
519
+ tasks = tasks.slice(0, limit);
520
+ }
521
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully (single page)', {
522
+ method: 'getTasks',
523
+ count: tasks.length,
524
+ workspaceId,
525
+ projectId,
526
+ limitApplied: limit
527
+ });
528
+ return tasks;
529
+ }
530
+ catch (error) {
531
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch tasks', {
532
+ method: 'getTasks',
533
+ error: getErrorMessage(error),
534
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
535
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
536
+ });
537
+ throw this.formatApiError(error, 'fetch tasks');
538
+ }
539
+ }
540
+ async getTask(taskId) {
541
+ try {
542
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching single task from Motion API', {
543
+ method: 'getTask',
544
+ taskId
545
+ });
546
+ const response = await this.requestWithRetry(() => this.client.get(`/tasks/${taskId}`));
547
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Successfully fetched task', {
548
+ method: 'getTask',
549
+ taskId,
550
+ taskName: response.data.name
551
+ });
552
+ return response.data;
553
+ }
554
+ catch (error) {
555
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch task', {
556
+ method: 'getTask',
557
+ taskId,
558
+ error: getErrorMessage(error),
559
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
560
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
561
+ });
562
+ throw this.formatApiError(error, 'fetch task');
563
+ }
564
+ }
565
+ async createTask(taskData) {
566
+ try {
567
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating task in Motion API', {
568
+ method: 'createTask',
569
+ taskName: taskData.name,
570
+ workspaceId: taskData.workspaceId,
571
+ projectId: taskData.projectId
572
+ });
573
+ if (!taskData.workspaceId) {
574
+ throw new Error('Workspace ID is required to create a task');
575
+ }
576
+ // Create minimal payload by removing empty/null values to avoid validation errors
577
+ const minimalPayload = (0, constants_1.createMinimalPayload)(taskData);
578
+ // Debug logging: log the exact payload being sent to API
579
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'API payload for task creation', {
580
+ method: 'createTask',
581
+ payload: JSON.stringify(minimalPayload, null, 2)
582
+ });
583
+ const response = await this.requestWithRetry(() => this.client.post('/tasks', minimalPayload));
584
+ // Invalidate task-related caches after successful creation
585
+ // Note: Task cache would need to be implemented separately if needed
586
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task created successfully', {
587
+ method: 'createTask',
588
+ taskId: response.data.id,
589
+ taskName: response.data.name
590
+ });
591
+ return response.data;
592
+ }
593
+ catch (error) {
594
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create task', {
595
+ method: 'createTask',
596
+ error: getErrorMessage(error),
597
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
598
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
599
+ fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
600
+ });
601
+ throw this.formatApiError(error, 'create task');
602
+ }
603
+ }
604
+ async updateTask(taskId, updates) {
605
+ try {
606
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Updating task in Motion API', {
607
+ method: 'updateTask',
608
+ taskId,
609
+ updates: Object.keys(updates)
610
+ });
611
+ // Create minimal payload by removing empty/null values to avoid validation errors
612
+ const minimalUpdates = (0, constants_1.createMinimalPayload)(updates);
613
+ const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}`, minimalUpdates));
614
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task updated successfully', {
615
+ method: 'updateTask',
616
+ taskId,
617
+ taskName: response.data.name
618
+ });
619
+ return response.data;
620
+ }
621
+ catch (error) {
622
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to update task', {
623
+ method: 'updateTask',
624
+ taskId,
625
+ error: getErrorMessage(error),
626
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
627
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
628
+ });
629
+ throw this.formatApiError(error, 'update task');
630
+ }
631
+ }
632
+ async deleteTask(taskId) {
633
+ try {
634
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting task from Motion API', {
635
+ method: 'deleteTask',
636
+ taskId
637
+ });
638
+ await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}`));
639
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task deleted successfully', {
640
+ method: 'deleteTask',
641
+ taskId
642
+ });
643
+ }
644
+ catch (error) {
645
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete task', {
646
+ method: 'deleteTask',
647
+ taskId,
648
+ error: getErrorMessage(error),
649
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
650
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
651
+ });
652
+ throw this.formatApiError(error, 'delete task');
653
+ }
654
+ }
655
+ async moveTask(taskId, targetProjectId, targetWorkspaceId) {
656
+ try {
657
+ if (!targetProjectId && !targetWorkspaceId) {
658
+ throw new Error('Either targetProjectId or targetWorkspaceId must be provided');
659
+ }
660
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Moving task in Motion API', {
661
+ method: 'moveTask',
662
+ taskId,
663
+ targetProjectId,
664
+ targetWorkspaceId
665
+ });
666
+ const moveData = {};
667
+ if (targetProjectId)
668
+ moveData.projectId = targetProjectId;
669
+ if (targetWorkspaceId)
670
+ moveData.workspaceId = targetWorkspaceId;
671
+ const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}/move`, moveData));
672
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task moved successfully', {
673
+ method: 'moveTask',
674
+ taskId,
675
+ targetProjectId,
676
+ targetWorkspaceId
677
+ });
678
+ // TODO: Invalidate task cache for source and destination projects/workspaces when implemented
679
+ return response.data;
680
+ }
681
+ catch (error) {
682
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to move task', {
683
+ method: 'moveTask',
684
+ taskId,
685
+ targetProjectId,
686
+ targetWorkspaceId,
687
+ error: getErrorMessage(error),
688
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
689
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
690
+ });
691
+ throw this.formatApiError(error, 'move task');
692
+ }
693
+ }
694
+ async unassignTask(taskId) {
695
+ try {
696
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Unassigning task in Motion API', {
697
+ method: 'unassignTask',
698
+ taskId
699
+ });
700
+ const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}/unassign`));
701
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task unassigned successfully', {
702
+ method: 'unassignTask',
703
+ taskId
704
+ });
705
+ // TODO: Invalidate task cache for this task and any assignee-related caches when implemented
706
+ return response.data;
707
+ }
708
+ catch (error) {
709
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to unassign task', {
710
+ method: 'unassignTask',
711
+ taskId,
712
+ error: getErrorMessage(error),
713
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
714
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
715
+ });
716
+ throw this.formatApiError(error, 'unassign task');
717
+ }
718
+ }
719
+ async getWorkspaces() {
720
+ return this.workspaceCache.withCache('workspaces', async () => {
721
+ try {
722
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching workspaces from Motion API', {
723
+ method: 'getWorkspaces'
724
+ });
725
+ const response = await this.requestWithRetry(() => this.client.get('/workspaces'));
726
+ // Validate the response structure
727
+ const validatedResponse = this.validateResponse(response.data, motion_1.WorkspacesListResponseSchema, 'getWorkspaces');
728
+ // Extract workspaces array (handle both wrapped and unwrapped responses)
729
+ const workspaces = Array.isArray(validatedResponse)
730
+ ? validatedResponse
731
+ : validatedResponse.workspaces;
732
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Workspaces fetched and cached successfully', {
733
+ method: 'getWorkspaces',
734
+ count: workspaces.length,
735
+ workspaceNames: workspaces.map((w) => w.name)
736
+ });
737
+ return workspaces;
738
+ }
739
+ catch (error) {
740
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch workspaces', {
741
+ method: 'getWorkspaces',
742
+ error: getErrorMessage(error),
743
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
744
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
745
+ });
746
+ throw this.formatApiError(error, 'fetch workspaces');
747
+ }
748
+ });
749
+ }
750
+ async getUsers(workspaceId) {
751
+ const cacheKey = workspaceId ? `users:workspace:${workspaceId}` : 'users:all';
752
+ return this.userCache.withCache(cacheKey, async () => {
753
+ try {
754
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching users from Motion API', {
755
+ method: 'getUsers',
756
+ workspaceId
757
+ });
758
+ const params = new URLSearchParams();
759
+ if (workspaceId) {
760
+ params.append('workspaceId', workspaceId);
761
+ }
762
+ const queryString = params.toString();
763
+ const url = queryString ? `/users?${queryString}` : '/users';
764
+ const response = await this.requestWithRetry(() => this.client.get(url));
765
+ // The Motion API might wrap the users in a 'users' array
766
+ const usersData = response.data?.users || response.data || [];
767
+ const users = Array.isArray(usersData) ? usersData : [];
768
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Users fetched successfully', {
769
+ method: 'getUsers',
770
+ count: users.length,
771
+ workspaceId
772
+ });
773
+ return users;
774
+ }
775
+ catch (error) {
776
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch users', {
777
+ method: 'getUsers',
778
+ error: getErrorMessage(error),
779
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
780
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
781
+ });
782
+ throw this.formatApiError(error, 'fetch users');
783
+ }
784
+ });
785
+ }
786
+ async getCurrentUser() {
787
+ const cacheKey = 'currentUser';
788
+ // Use userCache but with a special single-user wrapper
789
+ const cachedUsers = await this.userCache.withCache(cacheKey, async () => {
790
+ try {
791
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching current user from Motion API', {
792
+ method: 'getCurrentUser'
793
+ });
794
+ const response = await this.requestWithRetry(() => this.client.get('/users/me'));
795
+ const user = response.data;
796
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Current user fetched successfully', {
797
+ method: 'getCurrentUser',
798
+ userId: user.id,
799
+ email: user.email
800
+ });
801
+ return [user]; // Wrap in array for cache compatibility
802
+ }
803
+ catch (error) {
804
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch current user', {
805
+ method: 'getCurrentUser',
806
+ error: getErrorMessage(error),
807
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
808
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
809
+ });
810
+ throw this.formatApiError(error, 'fetch current user');
811
+ }
812
+ });
813
+ return cachedUsers[0]; // Return just the user object
814
+ }
815
+ // Additional methods for intelligent features
816
+ /**
817
+ * Resolves a project identifier (either projectId or projectName) to a MotionProject
818
+ * Searches across all workspaces if not found in the specified workspace
819
+ * @param identifier Object containing either projectId or projectName
820
+ * @param workspaceId Workspace to start searching in
821
+ * @returns Resolved MotionProject with workspace info, or undefined if not found
822
+ */
823
+ async resolveProjectIdentifier(identifier, workspaceId) {
824
+ try {
825
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Resolving project identifier', {
826
+ method: 'resolveProjectIdentifier',
827
+ projectId: identifier.projectId,
828
+ projectName: identifier.projectName,
829
+ workspaceId
830
+ });
831
+ // If projectId is provided, try to get it directly
832
+ if (identifier.projectId) {
833
+ try {
834
+ const project = await this.getProject(identifier.projectId);
835
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project resolved by ID', {
836
+ method: 'resolveProjectIdentifier',
837
+ projectId: identifier.projectId,
838
+ projectName: project.name,
839
+ workspaceId: project.workspaceId
840
+ });
841
+ return project;
842
+ }
843
+ catch (error) {
844
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve project by ID', {
845
+ method: 'resolveProjectIdentifier',
846
+ projectId: identifier.projectId,
847
+ error: getErrorMessage(error)
848
+ });
849
+ // Fall through to projectName resolution if projectId fails
850
+ }
851
+ }
852
+ // If projectName is provided (or projectId failed), resolve by name across workspaces
853
+ if (identifier.projectName) {
854
+ const project = await this.getProjectByName(identifier.projectName, workspaceId);
855
+ if (project) {
856
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project resolved by name across workspaces', {
857
+ method: 'resolveProjectIdentifier',
858
+ projectName: identifier.projectName,
859
+ projectId: project.id,
860
+ foundInWorkspaceId: project.workspaceId
861
+ });
862
+ return project;
863
+ }
864
+ }
865
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve project identifier', {
866
+ method: 'resolveProjectIdentifier',
867
+ projectId: identifier.projectId,
868
+ projectName: identifier.projectName,
869
+ workspaceId
870
+ });
871
+ return undefined;
872
+ }
873
+ catch (error) {
874
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Error resolving project identifier', {
875
+ method: 'resolveProjectIdentifier',
876
+ projectId: identifier.projectId,
877
+ projectName: identifier.projectName,
878
+ error: getErrorMessage(error)
879
+ });
880
+ throw error;
881
+ }
882
+ }
883
+ /**
884
+ * Resolves a user identifier (either userId or userName/email) to a MotionUser
885
+ * Searches across all workspaces if not found in the specified workspace
886
+ * @param identifier Object containing either userId or userName
887
+ * @param workspaceId Workspace to start searching in (optional)
888
+ * @returns Resolved MotionUser with workspace info, or undefined if not found
889
+ */
890
+ async resolveUserIdentifier(identifier, workspaceId) {
891
+ try {
892
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Resolving user identifier', {
893
+ method: 'resolveUserIdentifier',
894
+ userId: identifier.userId,
895
+ userName: identifier.userName,
896
+ workspaceId
897
+ });
898
+ // If userId is provided, search by ID across all workspaces
899
+ if (identifier.userId) {
900
+ const allWorkspaces = await this.getWorkspaces();
901
+ for (const workspace of allWorkspaces) {
902
+ try {
903
+ const users = await this.getUsers(workspace.id);
904
+ const user = users.find(u => u.id === identifier.userId);
905
+ if (user) {
906
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'User resolved by ID', {
907
+ method: 'resolveUserIdentifier',
908
+ userId: identifier.userId,
909
+ userName: user.name,
910
+ foundInWorkspaceId: workspace.id
911
+ });
912
+ return user;
913
+ }
914
+ }
915
+ catch (workspaceError) {
916
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for user by ID', {
917
+ method: 'resolveUserIdentifier',
918
+ userId: identifier.userId,
919
+ workspaceId: workspace.id,
920
+ error: getErrorMessage(workspaceError)
921
+ });
922
+ }
923
+ }
924
+ }
925
+ // If userName is provided, search by name/email across all workspaces
926
+ if (identifier.userName) {
927
+ const allWorkspaces = await this.getWorkspaces();
928
+ const searchTerm = identifier.userName.toLowerCase();
929
+ for (const workspace of allWorkspaces) {
930
+ try {
931
+ const users = await this.getUsers(workspace.id);
932
+ const user = users.find(u => u.name?.toLowerCase().includes(searchTerm) ||
933
+ u.email?.toLowerCase().includes(searchTerm));
934
+ if (user) {
935
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'User resolved by name/email', {
936
+ method: 'resolveUserIdentifier',
937
+ userName: identifier.userName,
938
+ userId: user.id,
939
+ foundInWorkspaceId: workspace.id
940
+ });
941
+ return user;
942
+ }
943
+ }
944
+ catch (workspaceError) {
945
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for user by name', {
946
+ method: 'resolveUserIdentifier',
947
+ userName: identifier.userName,
948
+ workspaceId: workspace.id,
949
+ error: getErrorMessage(workspaceError)
950
+ });
951
+ }
952
+ }
953
+ }
954
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve user identifier', {
955
+ method: 'resolveUserIdentifier',
956
+ userId: identifier.userId,
957
+ userName: identifier.userName,
958
+ workspaceId
959
+ });
960
+ return undefined;
961
+ }
962
+ catch (error) {
963
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Error resolving user identifier', {
964
+ method: 'resolveUserIdentifier',
965
+ userId: identifier.userId,
966
+ userName: identifier.userName,
967
+ error: getErrorMessage(error)
968
+ });
969
+ throw error;
970
+ }
971
+ }
972
+ async getProjectByName(projectName, workspaceId) {
973
+ try {
974
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Finding project by name', {
975
+ method: 'getProjectByName',
976
+ projectName,
977
+ workspaceId
978
+ });
979
+ // First, search in the specified workspace
980
+ const projects = await this.getProjects(workspaceId);
981
+ const project = projects.find(p => p.name === projectName);
982
+ if (project) {
983
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project found by name in specified workspace', {
984
+ method: 'getProjectByName',
985
+ projectName,
986
+ projectId: project.id,
987
+ workspaceId
988
+ });
989
+ return project;
990
+ }
991
+ // If not found in specified workspace, search across all other workspaces
992
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Project not found in specified workspace, searching all workspaces', {
993
+ method: 'getProjectByName',
994
+ projectName,
995
+ specifiedWorkspaceId: workspaceId
996
+ });
997
+ const allWorkspaces = await this.getWorkspaces();
998
+ const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
999
+ for (const workspace of otherWorkspaces) {
1000
+ try {
1001
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching workspace for project', {
1002
+ method: 'getProjectByName',
1003
+ projectName,
1004
+ searchingWorkspaceId: workspace.id,
1005
+ searchingWorkspaceName: workspace.name
1006
+ });
1007
+ const workspaceProjects = await this.getProjects(workspace.id);
1008
+ const foundProject = workspaceProjects.find(p => p.name === projectName);
1009
+ if (foundProject) {
1010
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project found by name in different workspace', {
1011
+ method: 'getProjectByName',
1012
+ projectName,
1013
+ projectId: foundProject.id,
1014
+ foundInWorkspaceId: workspace.id,
1015
+ foundInWorkspaceName: workspace.name,
1016
+ originalWorkspaceId: workspaceId
1017
+ });
1018
+ return foundProject;
1019
+ }
1020
+ }
1021
+ catch (workspaceError) {
1022
+ // Log error but continue searching other workspaces
1023
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for project', {
1024
+ method: 'getProjectByName',
1025
+ projectName,
1026
+ workspaceId: workspace.id,
1027
+ workspaceName: workspace.name,
1028
+ error: getErrorMessage(workspaceError)
1029
+ });
1030
+ }
1031
+ }
1032
+ // Project not found in any workspace
1033
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Project not found by name in any workspace', {
1034
+ method: 'getProjectByName',
1035
+ projectName,
1036
+ searchedWorkspaces: [workspaceId, ...otherWorkspaces.map(w => w.id)],
1037
+ totalWorkspacesSearched: allWorkspaces.length
1038
+ });
1039
+ return undefined;
1040
+ }
1041
+ catch (error) {
1042
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to find project by name', {
1043
+ method: 'getProjectByName',
1044
+ projectName,
1045
+ error: getErrorMessage(error)
1046
+ });
1047
+ throw error;
1048
+ }
1049
+ }
1050
+ async searchTasks(query, workspaceId, limit) {
1051
+ try {
1052
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching tasks', {
1053
+ method: 'searchTasks',
1054
+ query,
1055
+ workspaceId,
1056
+ limit
1057
+ });
1058
+ // Apply search limit to prevent resource exhaustion
1059
+ const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1060
+ const lowerQuery = query.toLowerCase();
1061
+ let allMatchingTasks = [];
1062
+ // First, search in the specified workspace
1063
+ const primaryTasks = await this.getTasks({
1064
+ workspaceId,
1065
+ limit: effectiveLimit,
1066
+ maxPages: constants_1.LIMITS.MAX_PAGES
1067
+ });
1068
+ const primaryMatches = primaryTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
1069
+ task.description?.toLowerCase().includes(lowerQuery));
1070
+ allMatchingTasks.push(...primaryMatches);
1071
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
1072
+ method: 'searchTasks',
1073
+ query,
1074
+ primaryWorkspaceId: workspaceId,
1075
+ primaryMatches: primaryMatches.length
1076
+ });
1077
+ // If we haven't reached the limit, search other workspaces
1078
+ if (allMatchingTasks.length < effectiveLimit) {
1079
+ try {
1080
+ const allWorkspaces = await this.getWorkspaces();
1081
+ const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
1082
+ for (const workspace of otherWorkspaces) {
1083
+ if (allMatchingTasks.length >= effectiveLimit)
1084
+ break;
1085
+ try {
1086
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for tasks', {
1087
+ method: 'searchTasks',
1088
+ query,
1089
+ searchingWorkspaceId: workspace.id,
1090
+ searchingWorkspaceName: workspace.name
1091
+ });
1092
+ const workspaceTasks = await this.getTasks({
1093
+ workspaceId: workspace.id,
1094
+ limit: effectiveLimit,
1095
+ maxPages: constants_1.LIMITS.MAX_PAGES
1096
+ });
1097
+ const workspaceMatches = workspaceTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
1098
+ task.description?.toLowerCase().includes(lowerQuery));
1099
+ allMatchingTasks.push(...workspaceMatches);
1100
+ if (workspaceMatches.length > 0) {
1101
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
1102
+ method: 'searchTasks',
1103
+ query,
1104
+ workspaceId: workspace.id,
1105
+ workspaceName: workspace.name,
1106
+ matches: workspaceMatches.length
1107
+ });
1108
+ }
1109
+ }
1110
+ catch (workspaceError) {
1111
+ // Log error but continue searching other workspaces
1112
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for tasks', {
1113
+ method: 'searchTasks',
1114
+ query,
1115
+ workspaceId: workspace.id,
1116
+ workspaceName: workspace.name,
1117
+ error: getErrorMessage(workspaceError)
1118
+ });
1119
+ }
1120
+ }
1121
+ }
1122
+ catch (workspaceListError) {
1123
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to get workspace list for cross-workspace search', {
1124
+ method: 'searchTasks',
1125
+ query,
1126
+ error: getErrorMessage(workspaceListError)
1127
+ });
1128
+ }
1129
+ }
1130
+ // Apply final limit and return results
1131
+ const finalResults = allMatchingTasks.slice(0, effectiveLimit);
1132
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task search completed across all workspaces', {
1133
+ method: 'searchTasks',
1134
+ query,
1135
+ totalMatches: allMatchingTasks.length,
1136
+ returnedResults: finalResults.length,
1137
+ limit: effectiveLimit,
1138
+ crossWorkspaceSearch: allMatchingTasks.length > primaryMatches.length
1139
+ });
1140
+ return finalResults;
1141
+ }
1142
+ catch (error) {
1143
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search tasks', {
1144
+ method: 'searchTasks',
1145
+ query,
1146
+ error: getErrorMessage(error)
1147
+ });
1148
+ throw error;
1149
+ }
1150
+ }
1151
+ async searchProjects(query, workspaceId, limit) {
1152
+ try {
1153
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching projects', {
1154
+ method: 'searchProjects',
1155
+ query,
1156
+ workspaceId,
1157
+ limit
1158
+ });
1159
+ // Apply search limit to prevent resource exhaustion
1160
+ const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1161
+ const lowerQuery = query.toLowerCase();
1162
+ let allMatchingProjects = [];
1163
+ // First, search in the specified workspace
1164
+ const primaryProjects = await this.getProjects(workspaceId, constants_1.LIMITS.MAX_PAGES);
1165
+ const primaryMatches = primaryProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
1166
+ project.description?.toLowerCase().includes(lowerQuery));
1167
+ allMatchingProjects.push(...primaryMatches);
1168
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
1169
+ method: 'searchProjects',
1170
+ query,
1171
+ primaryWorkspaceId: workspaceId,
1172
+ primaryMatches: primaryMatches.length
1173
+ });
1174
+ // If we haven't reached the limit, search other workspaces
1175
+ if (allMatchingProjects.length < effectiveLimit) {
1176
+ try {
1177
+ const allWorkspaces = await this.getWorkspaces();
1178
+ const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
1179
+ for (const workspace of otherWorkspaces) {
1180
+ if (allMatchingProjects.length >= effectiveLimit)
1181
+ break;
1182
+ try {
1183
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for projects', {
1184
+ method: 'searchProjects',
1185
+ query,
1186
+ searchingWorkspaceId: workspace.id,
1187
+ searchingWorkspaceName: workspace.name
1188
+ });
1189
+ const workspaceProjects = await this.getProjects(workspace.id, constants_1.LIMITS.MAX_PAGES);
1190
+ const workspaceMatches = workspaceProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
1191
+ project.description?.toLowerCase().includes(lowerQuery));
1192
+ allMatchingProjects.push(...workspaceMatches);
1193
+ if (workspaceMatches.length > 0) {
1194
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
1195
+ method: 'searchProjects',
1196
+ query,
1197
+ workspaceId: workspace.id,
1198
+ workspaceName: workspace.name,
1199
+ matches: workspaceMatches.length
1200
+ });
1201
+ }
1202
+ }
1203
+ catch (workspaceError) {
1204
+ // Log error but continue searching other workspaces
1205
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for projects', {
1206
+ method: 'searchProjects',
1207
+ query,
1208
+ workspaceId: workspace.id,
1209
+ workspaceName: workspace.name,
1210
+ error: getErrorMessage(workspaceError)
1211
+ });
1212
+ }
1213
+ }
1214
+ }
1215
+ catch (workspaceListError) {
1216
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to get workspace list for cross-workspace search', {
1217
+ method: 'searchProjects',
1218
+ query,
1219
+ error: getErrorMessage(workspaceListError)
1220
+ });
1221
+ }
1222
+ }
1223
+ // Apply final limit and return results
1224
+ const finalResults = allMatchingProjects.slice(0, effectiveLimit);
1225
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project search completed across all workspaces', {
1226
+ method: 'searchProjects',
1227
+ query,
1228
+ totalMatches: allMatchingProjects.length,
1229
+ returnedResults: finalResults.length,
1230
+ limit: effectiveLimit,
1231
+ crossWorkspaceSearch: allMatchingProjects.length > primaryMatches.length
1232
+ });
1233
+ return finalResults;
1234
+ }
1235
+ catch (error) {
1236
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search projects', {
1237
+ method: 'searchProjects',
1238
+ query,
1239
+ error: getErrorMessage(error)
1240
+ });
1241
+ throw error;
1242
+ }
1243
+ }
1244
+ /**
1245
+ * Get comments for a task with proper pagination support
1246
+ * @param taskId Task ID to get comments for
1247
+ * @param cursor Optional cursor for pagination
1248
+ * @returns Paginated response with comments and metadata
1249
+ */
1250
+ async getComments(taskId, cursor) {
1251
+ const cacheParams = { taskId, cursor: cursor || null };
1252
+ const cacheKey = `comments:${JSON.stringify(cacheParams)}`;
1253
+ return this.commentCache.withCache(cacheKey, async () => {
1254
+ try {
1255
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching comments from Motion API', {
1256
+ method: 'getComments',
1257
+ taskId,
1258
+ cursor
1259
+ });
1260
+ const params = new URLSearchParams({ taskId });
1261
+ if (cursor)
1262
+ params.append('cursor', cursor);
1263
+ const response = await this.requestWithRetry(() => this.client.get(`/comments?${params.toString()}`));
1264
+ // Use new response wrapper for consistent handling
1265
+ const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'comments');
1266
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Comments fetched successfully', {
1267
+ method: 'getComments',
1268
+ count: unwrapped.data.length,
1269
+ hasMore: !!unwrapped.meta?.nextCursor,
1270
+ taskId
1271
+ });
1272
+ // Return in our standard paginated format
1273
+ return {
1274
+ data: unwrapped.data,
1275
+ meta: {
1276
+ nextCursor: unwrapped.meta?.nextCursor,
1277
+ pageSize: unwrapped.meta?.pageSize || unwrapped.data.length
1278
+ }
1279
+ };
1280
+ }
1281
+ catch (error) {
1282
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch comments', {
1283
+ method: 'getComments',
1284
+ error: getErrorMessage(error),
1285
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1286
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1287
+ taskId,
1288
+ cursor
1289
+ });
1290
+ throw this.formatApiError(error, 'fetch comments');
1291
+ }
1292
+ });
1293
+ }
1294
+ async createComment(commentData) {
1295
+ try {
1296
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating comment in Motion API', {
1297
+ method: 'createComment',
1298
+ taskId: commentData.taskId,
1299
+ contentLength: commentData.content?.length || 0
1300
+ });
1301
+ // Create minimal payload by removing empty/null values to avoid validation errors
1302
+ const minimalPayload = (0, constants_1.createMinimalPayload)(commentData);
1303
+ const response = await this.requestWithRetry(() => this.client.post('/comments', minimalPayload));
1304
+ // Invalidate cache after successful creation
1305
+ const cacheKey = `comments:${JSON.stringify({ taskId: commentData.taskId, cursor: null })}`;
1306
+ this.commentCache.invalidate(cacheKey);
1307
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Comment created successfully', {
1308
+ method: 'createComment',
1309
+ commentId: response.data?.id,
1310
+ taskId: commentData.taskId
1311
+ });
1312
+ return response.data;
1313
+ }
1314
+ catch (error) {
1315
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create comment', {
1316
+ method: 'createComment',
1317
+ error: getErrorMessage(error),
1318
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1319
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1320
+ taskId: commentData?.taskId
1321
+ });
1322
+ throw this.formatApiError(error, 'create comment');
1323
+ }
1324
+ }
1325
+ /**
1326
+ * Fetch custom fields from Motion API
1327
+ * @param workspaceId - Required workspace ID to get custom fields for
1328
+ * @returns Array of custom fields
1329
+ */
1330
+ async getCustomFields(workspaceId) {
1331
+ const cacheKey = `custom-fields:${workspaceId}`;
1332
+ return this.customFieldCache.withCache(cacheKey, async () => {
1333
+ try {
1334
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching custom fields from Motion API', {
1335
+ method: 'getCustomFields',
1336
+ workspaceId
1337
+ });
1338
+ const url = `/beta/workspaces/${workspaceId}/custom-fields`;
1339
+ const response = await this.requestWithRetry(() => this.client.get(url));
1340
+ // Beta API returns direct array, not wrapped
1341
+ const fieldsArray = response.data || [];
1342
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom fields fetched successfully', {
1343
+ method: 'getCustomFields',
1344
+ count: fieldsArray.length,
1345
+ workspaceId
1346
+ });
1347
+ return fieldsArray;
1348
+ }
1349
+ catch (error) {
1350
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch custom fields', {
1351
+ method: 'getCustomFields',
1352
+ error: getErrorMessage(error),
1353
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1354
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1355
+ workspaceId
1356
+ });
1357
+ throw this.formatApiError(error, 'fetch custom fields');
1358
+ }
1359
+ });
1360
+ }
1361
+ /**
1362
+ * Create a new custom field
1363
+ * @param workspaceId - Required workspace ID to create custom field in
1364
+ * @param fieldData - Data for creating the custom field
1365
+ * @returns The created custom field
1366
+ */
1367
+ async createCustomField(workspaceId, fieldData) {
1368
+ try {
1369
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating custom field in Motion API', {
1370
+ method: 'createCustomField',
1371
+ name: fieldData.name,
1372
+ field: fieldData.field,
1373
+ workspaceId
1374
+ });
1375
+ // Transform payload to match Motion API expectations
1376
+ // POST API expects 'type' in request, but returns 'field' in response
1377
+ const apiPayload = {
1378
+ name: fieldData.name,
1379
+ type: fieldData.field, // Motion API POST expects 'type' property in request body
1380
+ ...(fieldData.metadata && { metadata: fieldData.metadata })
1381
+ };
1382
+ // Create minimal payload by removing empty/null values to avoid validation errors
1383
+ const minimalPayload = (0, constants_1.createMinimalPayload)(apiPayload);
1384
+ const response = await this.requestWithRetry(() => this.client.post(`/beta/workspaces/${workspaceId}/custom-fields`, minimalPayload));
1385
+ // Invalidate cache after successful creation
1386
+ this.customFieldCache.invalidate(`custom-fields:${workspaceId}`);
1387
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field created successfully', {
1388
+ method: 'createCustomField',
1389
+ fieldId: response.data?.id,
1390
+ name: fieldData.name,
1391
+ workspaceId
1392
+ });
1393
+ return response.data;
1394
+ }
1395
+ catch (error) {
1396
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create custom field', {
1397
+ method: 'createCustomField',
1398
+ error: getErrorMessage(error),
1399
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1400
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1401
+ fieldName: fieldData?.name,
1402
+ workspaceId
1403
+ });
1404
+ throw this.formatApiError(error, 'create custom field');
1405
+ }
1406
+ }
1407
+ /**
1408
+ * Delete a custom field
1409
+ * @param workspaceId - Required workspace ID containing the custom field
1410
+ * @param fieldId - ID of the custom field to delete
1411
+ * @returns Success indicator
1412
+ */
1413
+ async deleteCustomField(workspaceId, fieldId) {
1414
+ try {
1415
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting custom field from Motion API', {
1416
+ method: 'deleteCustomField',
1417
+ fieldId,
1418
+ workspaceId
1419
+ });
1420
+ await this.requestWithRetry(() => this.client.delete(`/beta/workspaces/${workspaceId}/custom-fields/${fieldId}`));
1421
+ // Invalidate cache after successful deletion
1422
+ this.customFieldCache.invalidate(`custom-fields:${workspaceId}`);
1423
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field deleted successfully', {
1424
+ method: 'deleteCustomField',
1425
+ fieldId,
1426
+ workspaceId
1427
+ });
1428
+ return { success: true };
1429
+ }
1430
+ catch (error) {
1431
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete custom field', {
1432
+ method: 'deleteCustomField',
1433
+ error: getErrorMessage(error),
1434
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1435
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1436
+ fieldId,
1437
+ workspaceId
1438
+ });
1439
+ throw this.formatApiError(error, 'delete custom field');
1440
+ }
1441
+ }
1442
+ /**
1443
+ * Add a custom field to a project
1444
+ * @param projectId - ID of the project
1445
+ * @param fieldId - ID of the custom field
1446
+ * @param value - Optional value for the field
1447
+ * @returns Updated project data
1448
+ */
1449
+ async addCustomFieldToProject(projectId, fieldId, value) {
1450
+ try {
1451
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Adding custom field to project', {
1452
+ method: 'addCustomFieldToProject',
1453
+ projectId,
1454
+ fieldId,
1455
+ hasValue: value !== undefined
1456
+ });
1457
+ const requestData = {
1458
+ fieldId,
1459
+ ...(value !== undefined && { value })
1460
+ };
1461
+ const response = await this.requestWithRetry(() => this.client.post(`/projects/${projectId}/custom-fields`, requestData));
1462
+ // Invalidate project cache for specific workspace if available
1463
+ if (response.data?.workspaceId) {
1464
+ this.projectCache.invalidate(`projects:workspace:${response.data.workspaceId}`);
1465
+ }
1466
+ else {
1467
+ // Fallback to broader invalidation if workspace unknown
1468
+ this.projectCache.invalidate(`projects:`);
1469
+ }
1470
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to project successfully', {
1471
+ method: 'addCustomFieldToProject',
1472
+ projectId,
1473
+ fieldId
1474
+ });
1475
+ return response.data;
1476
+ }
1477
+ catch (error) {
1478
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to add custom field to project', {
1479
+ method: 'addCustomFieldToProject',
1480
+ error: getErrorMessage(error),
1481
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1482
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1483
+ projectId,
1484
+ fieldId
1485
+ });
1486
+ throw this.formatApiError(error, 'add custom field to project');
1487
+ }
1488
+ }
1489
+ /**
1490
+ * Remove a custom field from a project
1491
+ * @param projectId - ID of the project
1492
+ * @param fieldId - ID of the custom field
1493
+ * @returns Success indicator
1494
+ */
1495
+ async removeCustomFieldFromProject(projectId, fieldId) {
1496
+ try {
1497
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Removing custom field from project', {
1498
+ method: 'removeCustomFieldFromProject',
1499
+ projectId,
1500
+ fieldId
1501
+ });
1502
+ 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
1505
+ this.projectCache.invalidate(`projects:`);
1506
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from project successfully', {
1507
+ method: 'removeCustomFieldFromProject',
1508
+ projectId,
1509
+ fieldId
1510
+ });
1511
+ return { success: true };
1512
+ }
1513
+ catch (error) {
1514
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to remove custom field from project', {
1515
+ method: 'removeCustomFieldFromProject',
1516
+ error: getErrorMessage(error),
1517
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1518
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1519
+ projectId,
1520
+ fieldId
1521
+ });
1522
+ throw this.formatApiError(error, 'remove custom field from project');
1523
+ }
1524
+ }
1525
+ /**
1526
+ * Add a custom field to a task
1527
+ * @param taskId - ID of the task
1528
+ * @param fieldId - ID of the custom field
1529
+ * @param value - Optional value for the field
1530
+ * @returns Updated task data
1531
+ */
1532
+ async addCustomFieldToTask(taskId, fieldId, value) {
1533
+ try {
1534
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Adding custom field to task', {
1535
+ method: 'addCustomFieldToTask',
1536
+ taskId,
1537
+ fieldId,
1538
+ hasValue: value !== undefined
1539
+ });
1540
+ const requestData = {
1541
+ fieldId,
1542
+ ...(value !== undefined && { value })
1543
+ };
1544
+ 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
+ // }
1549
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to task successfully', {
1550
+ method: 'addCustomFieldToTask',
1551
+ taskId,
1552
+ fieldId
1553
+ });
1554
+ return response.data;
1555
+ }
1556
+ catch (error) {
1557
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to add custom field to task', {
1558
+ method: 'addCustomFieldToTask',
1559
+ error: getErrorMessage(error),
1560
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1561
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1562
+ taskId,
1563
+ fieldId
1564
+ });
1565
+ throw this.formatApiError(error, 'add custom field to task');
1566
+ }
1567
+ }
1568
+ /**
1569
+ * Remove a custom field from a task
1570
+ * @param taskId - ID of the task
1571
+ * @param fieldId - ID of the custom field
1572
+ * @returns Success indicator
1573
+ */
1574
+ async removeCustomFieldFromTask(taskId, fieldId) {
1575
+ try {
1576
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Removing custom field from task', {
1577
+ method: 'removeCustomFieldFromTask',
1578
+ taskId,
1579
+ fieldId
1580
+ });
1581
+ await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}/custom-fields/${fieldId}`));
1582
+ // TODO: Invalidate task cache when implemented
1583
+ // this.taskCache.invalidate(`tasks:`);
1584
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from task successfully', {
1585
+ method: 'removeCustomFieldFromTask',
1586
+ taskId,
1587
+ fieldId
1588
+ });
1589
+ return { success: true };
1590
+ }
1591
+ catch (error) {
1592
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to remove custom field from task', {
1593
+ method: 'removeCustomFieldFromTask',
1594
+ error: getErrorMessage(error),
1595
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1596
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1597
+ taskId,
1598
+ fieldId
1599
+ });
1600
+ throw this.formatApiError(error, 'remove custom field from task');
1601
+ }
1602
+ }
1603
+ /**
1604
+ * Fetch recurring tasks from Motion API with automatic pagination
1605
+ * @param workspaceId - Optional workspace ID to filter recurring tasks
1606
+ * @param maxPages - Maximum number of pages to fetch (default: 10)
1607
+ * @returns Array of recurring tasks from all pages
1608
+ */
1609
+ async getRecurringTasks(workspaceId, maxPages = 10) {
1610
+ const cacheKey = workspaceId ? `recurring-tasks:workspace:${workspaceId}` : 'recurring-tasks:all';
1611
+ return this.recurringTaskCache.withCache(cacheKey, async () => {
1612
+ try {
1613
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching recurring tasks from Motion API with pagination', {
1614
+ method: 'getRecurringTasks',
1615
+ workspaceId,
1616
+ maxPages
1617
+ });
1618
+ // Create a fetch function for pagination utility
1619
+ const fetchPage = async (cursor) => {
1620
+ const params = new URLSearchParams();
1621
+ if (workspaceId)
1622
+ params.append('workspaceId', workspaceId);
1623
+ if (cursor)
1624
+ params.append('cursor', cursor);
1625
+ const queryString = params.toString();
1626
+ const url = queryString ? `/recurring-tasks?${queryString}` : '/recurring-tasks';
1627
+ return this.requestWithRetry(() => this.client.get(url));
1628
+ };
1629
+ // Use pagination utility to fetch all pages
1630
+ const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'recurring-tasks', {
1631
+ maxPages,
1632
+ logProgress: true
1633
+ });
1634
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring tasks fetched successfully with pagination', {
1635
+ method: 'getRecurringTasks',
1636
+ totalCount: paginatedResult.totalFetched,
1637
+ pagesProcessed: Math.ceil(paginatedResult.totalFetched / 50), // Assuming ~50 items per page
1638
+ hasMore: paginatedResult.hasMore,
1639
+ workspaceId
1640
+ });
1641
+ return paginatedResult.items;
1642
+ }
1643
+ catch (error) {
1644
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch recurring tasks', {
1645
+ method: 'getRecurringTasks',
1646
+ error: getErrorMessage(error),
1647
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1648
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1649
+ workspaceId
1650
+ });
1651
+ throw this.formatApiError(error, 'fetch recurring tasks');
1652
+ }
1653
+ });
1654
+ }
1655
+ /**
1656
+ * Create a new recurring task
1657
+ * @param taskData - Data for creating the recurring task
1658
+ * @returns The created recurring task
1659
+ */
1660
+ async createRecurringTask(taskData) {
1661
+ try {
1662
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating recurring task in Motion API', {
1663
+ method: 'createRecurringTask',
1664
+ name: taskData.name,
1665
+ assigneeId: taskData.assigneeId,
1666
+ frequency: taskData.frequency.type,
1667
+ workspaceId: taskData.workspaceId
1668
+ });
1669
+ // Create minimal payload by removing empty/null values to avoid validation errors
1670
+ const minimalPayload = (0, constants_1.createMinimalPayload)(taskData);
1671
+ const response = await this.requestWithRetry(() => this.client.post('/recurring-tasks', minimalPayload));
1672
+ // Invalidate cache after successful creation
1673
+ this.recurringTaskCache.invalidate('recurring-tasks:');
1674
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring task created successfully', {
1675
+ method: 'createRecurringTask',
1676
+ taskId: response.data?.id,
1677
+ name: taskData.name
1678
+ });
1679
+ return response.data;
1680
+ }
1681
+ catch (error) {
1682
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create recurring task', {
1683
+ method: 'createRecurringTask',
1684
+ error: getErrorMessage(error),
1685
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1686
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1687
+ taskName: taskData?.name
1688
+ });
1689
+ throw this.formatApiError(error, 'create recurring task');
1690
+ }
1691
+ }
1692
+ /**
1693
+ * Delete a recurring task
1694
+ * @param recurringTaskId - ID of the recurring task to delete
1695
+ * @returns Success indicator
1696
+ */
1697
+ async deleteRecurringTask(recurringTaskId) {
1698
+ try {
1699
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting recurring task from Motion API', {
1700
+ method: 'deleteRecurringTask',
1701
+ recurringTaskId
1702
+ });
1703
+ await this.requestWithRetry(() => this.client.delete(`/recurring-tasks/${recurringTaskId}`));
1704
+ // Invalidate cache after successful deletion
1705
+ this.recurringTaskCache.invalidate('recurring-tasks:');
1706
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring task deleted successfully', {
1707
+ method: 'deleteRecurringTask',
1708
+ recurringTaskId
1709
+ });
1710
+ return { success: true };
1711
+ }
1712
+ catch (error) {
1713
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete recurring task', {
1714
+ method: 'deleteRecurringTask',
1715
+ error: getErrorMessage(error),
1716
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1717
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1718
+ recurringTaskId
1719
+ });
1720
+ throw this.formatApiError(error, 'delete recurring task');
1721
+ }
1722
+ }
1723
+ /**
1724
+ * Get available schedule names for auto-scheduling
1725
+ * @param workspaceId - Optional workspace ID to filter schedules (currently unused by Motion API)
1726
+ * @returns Array of schedule names
1727
+ */
1728
+ async getAvailableScheduleNames(workspaceId) {
1729
+ try {
1730
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching available schedule names', {
1731
+ method: 'getAvailableScheduleNames',
1732
+ workspaceId
1733
+ });
1734
+ // Fetch all schedules without filters to get available schedule templates
1735
+ const schedules = await this.getSchedules();
1736
+ const scheduleNames = schedules.map(schedule => schedule.name).filter(Boolean);
1737
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Available schedule names fetched successfully', {
1738
+ method: 'getAvailableScheduleNames',
1739
+ count: scheduleNames.length,
1740
+ scheduleNames
1741
+ });
1742
+ return scheduleNames;
1743
+ }
1744
+ catch (error) {
1745
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch available schedule names', {
1746
+ method: 'getAvailableScheduleNames',
1747
+ error: getErrorMessage(error),
1748
+ workspaceId
1749
+ });
1750
+ throw this.formatApiError(error, 'fetch available schedule names');
1751
+ }
1752
+ }
1753
+ /**
1754
+ * Fetch schedules from Motion API
1755
+ * @param userId - Optional user ID to filter schedules
1756
+ * @param startDate - Optional start date (ISO 8601) to filter schedules
1757
+ * @param endDate - Optional end date (ISO 8601) to filter schedules
1758
+ * @returns Array of schedules
1759
+ */
1760
+ async getSchedules(userId, startDate, endDate) {
1761
+ // Use JSON.stringify for deterministic cache key generation to avoid collisions
1762
+ const cacheParams = { userId: userId || null, startDate: startDate || null, endDate: endDate || null };
1763
+ const cacheKey = `schedules:${JSON.stringify(cacheParams)}`;
1764
+ return this.scheduleCache.withCache(cacheKey, async () => {
1765
+ try {
1766
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching schedules from Motion API', {
1767
+ method: 'getSchedules',
1768
+ userId,
1769
+ startDate,
1770
+ endDate
1771
+ });
1772
+ const params = new URLSearchParams();
1773
+ if (userId)
1774
+ params.append('userId', userId);
1775
+ if (startDate)
1776
+ params.append('startDate', startDate);
1777
+ if (endDate)
1778
+ params.append('endDate', endDate);
1779
+ const queryString = params.toString();
1780
+ const url = queryString ? `/schedules?${queryString}` : '/schedules';
1781
+ const response = await this.requestWithRetry(() => this.client.get(url));
1782
+ // Validate response against schema
1783
+ const validatedResponse = this.validateResponse(response.data, motion_1.SchedulesListResponseSchema, 'getSchedules');
1784
+ // Handle both wrapped and unwrapped responses
1785
+ const schedules = Array.isArray(validatedResponse)
1786
+ ? validatedResponse
1787
+ : validatedResponse.schedules || [];
1788
+ const schedulesArray = Array.isArray(schedules) ? schedules : [];
1789
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Schedules fetched successfully', {
1790
+ method: 'getSchedules',
1791
+ count: schedulesArray.length,
1792
+ userId,
1793
+ startDate,
1794
+ endDate
1795
+ });
1796
+ return schedulesArray;
1797
+ }
1798
+ catch (error) {
1799
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch schedules', {
1800
+ method: 'getSchedules',
1801
+ error: getErrorMessage(error),
1802
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1803
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1804
+ userId,
1805
+ startDate,
1806
+ endDate
1807
+ });
1808
+ throw this.formatApiError(error, 'fetch schedules');
1809
+ }
1810
+ });
1811
+ }
1812
+ /**
1813
+ * Retrieves available workflow statuses from Motion
1814
+ * @param workspaceId - Optional workspace ID to filter statuses
1815
+ * @returns Promise resolving to array of Motion statuses
1816
+ * @throws {Error} If the API request fails
1817
+ */
1818
+ async getStatuses(workspaceId) {
1819
+ // Use workspace ID for cache key, or 'all' if not specified
1820
+ const cacheKey = workspaceId ? `statuses:workspace:${workspaceId}` : 'statuses:all';
1821
+ return this.statusCache.withCache(cacheKey, async () => {
1822
+ try {
1823
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching statuses from Motion API', {
1824
+ method: 'getStatuses',
1825
+ workspaceId
1826
+ });
1827
+ const params = new URLSearchParams();
1828
+ if (workspaceId)
1829
+ params.append('workspaceId', workspaceId);
1830
+ const queryString = params.toString();
1831
+ const url = queryString ? `/statuses?${queryString}` : '/statuses';
1832
+ const response = await this.requestWithRetry(() => this.client.get(url));
1833
+ // Validate response against schema
1834
+ const validatedResponse = this.validateResponse(response.data, motion_1.StatusesListResponseSchema, 'statuses');
1835
+ // Extract statuses from validated response
1836
+ const statuses = Array.isArray(validatedResponse)
1837
+ ? validatedResponse
1838
+ : ('statuses' in validatedResponse && Array.isArray(validatedResponse.statuses))
1839
+ ? validatedResponse.statuses
1840
+ : [];
1841
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Statuses fetched successfully', {
1842
+ method: 'getStatuses',
1843
+ count: statuses.length,
1844
+ workspaceId
1845
+ });
1846
+ return statuses;
1847
+ }
1848
+ catch (error) {
1849
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch statuses', {
1850
+ method: 'getStatuses',
1851
+ error: getErrorMessage(error),
1852
+ apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1853
+ apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1854
+ workspaceId
1855
+ });
1856
+ throw this.formatApiError(error, 'fetch statuses');
1857
+ }
1858
+ });
1859
+ }
1860
+ /**
1861
+ * Get all uncompleted tasks across all workspaces and projects
1862
+ * Filters tasks where status.isResolvedStatus is false or undefined
1863
+ */
1864
+ async getAllUncompletedTasks(limit) {
1865
+ try {
1866
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching all uncompleted tasks across workspaces', {
1867
+ method: 'getAllUncompletedTasks',
1868
+ limit
1869
+ });
1870
+ // Apply limit to prevent resource exhaustion
1871
+ const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1872
+ const allUncompletedTasks = [];
1873
+ try {
1874
+ // Get all workspaces
1875
+ const workspaces = await this.getWorkspaces();
1876
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching for uncompleted tasks across workspaces', {
1877
+ method: 'getAllUncompletedTasks',
1878
+ totalWorkspaces: workspaces.length
1879
+ });
1880
+ // Fetch tasks from each workspace
1881
+ for (const workspace of workspaces) {
1882
+ if (allUncompletedTasks.length >= effectiveLimit) {
1883
+ break; // Stop if we've reached the limit
1884
+ }
1885
+ try {
1886
+ // Get all tasks from this workspace (all projects)
1887
+ const workspaceTasks = await this.getTasks({
1888
+ workspaceId: workspace.id,
1889
+ limit: effectiveLimit,
1890
+ maxPages: constants_1.LIMITS.MAX_PAGES
1891
+ });
1892
+ // Filter for uncompleted tasks
1893
+ const uncompletedTasks = workspaceTasks.filter(task => {
1894
+ // Task is uncompleted if status is missing or isResolvedStatus is false
1895
+ if (!task.status)
1896
+ return true; // No status = not resolved
1897
+ if (typeof task.status === 'string')
1898
+ return true; // Simple string status = assume not resolved
1899
+ return !task.status.isResolvedStatus; // Object status with isResolvedStatus false
1900
+ });
1901
+ allUncompletedTasks.push(...uncompletedTasks);
1902
+ if (uncompletedTasks.length > 0) {
1903
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found uncompleted tasks in workspace', {
1904
+ method: 'getAllUncompletedTasks',
1905
+ workspaceId: workspace.id,
1906
+ workspaceName: workspace.name,
1907
+ uncompletedTasks: uncompletedTasks.length,
1908
+ totalTasks: workspaceTasks.length
1909
+ });
1910
+ }
1911
+ }
1912
+ catch (workspaceError) {
1913
+ // Log error but continue with other workspaces
1914
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to fetch tasks from workspace', {
1915
+ method: 'getAllUncompletedTasks',
1916
+ workspaceId: workspace.id,
1917
+ workspaceName: workspace.name,
1918
+ error: getErrorMessage(workspaceError)
1919
+ });
1920
+ }
1921
+ }
1922
+ }
1923
+ catch (workspaceListError) {
1924
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to get workspace list', {
1925
+ method: 'getAllUncompletedTasks',
1926
+ error: getErrorMessage(workspaceListError)
1927
+ });
1928
+ throw workspaceListError;
1929
+ }
1930
+ // Apply final limit and return results
1931
+ const finalResults = allUncompletedTasks.slice(0, effectiveLimit);
1932
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'All uncompleted tasks fetched successfully', {
1933
+ method: 'getAllUncompletedTasks',
1934
+ totalFound: allUncompletedTasks.length,
1935
+ returned: finalResults.length,
1936
+ limit: effectiveLimit
1937
+ });
1938
+ return finalResults;
1939
+ }
1940
+ catch (error) {
1941
+ (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch all uncompleted tasks', {
1942
+ method: 'getAllUncompletedTasks',
1943
+ error: getErrorMessage(error)
1944
+ });
1945
+ throw error;
1946
+ }
1947
+ }
1948
+ }
1949
+ exports.MotionApiService = MotionApiService;
1950
+ //# sourceMappingURL=motionApi.js.map