motionmcp 2.6.0 → 2.8.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 (125) hide show
  1. package/README.md +4 -4
  2. package/dist/handlers/CustomFieldHandler.d.ts.map +1 -1
  3. package/dist/handlers/CustomFieldHandler.js +18 -2
  4. package/dist/handlers/CustomFieldHandler.js.map +1 -1
  5. package/dist/handlers/HandlerFactory.d.ts +0 -5
  6. package/dist/handlers/HandlerFactory.d.ts.map +1 -1
  7. package/dist/handlers/HandlerFactory.js +0 -15
  8. package/dist/handlers/HandlerFactory.js.map +1 -1
  9. package/dist/handlers/RecurringTaskHandler.d.ts.map +1 -1
  10. package/dist/handlers/RecurringTaskHandler.js +7 -2
  11. package/dist/handlers/RecurringTaskHandler.js.map +1 -1
  12. package/dist/handlers/TaskHandler.d.ts.map +1 -1
  13. package/dist/handlers/TaskHandler.js +7 -0
  14. package/dist/handlers/TaskHandler.js.map +1 -1
  15. package/dist/mcp-server.js +3 -3
  16. package/dist/mcp-server.js.map +1 -1
  17. package/dist/services/api/ApiClient.d.ts +22 -0
  18. package/dist/services/api/ApiClient.d.ts.map +1 -0
  19. package/dist/services/api/ApiClient.js +198 -0
  20. package/dist/services/api/ApiClient.js.map +1 -0
  21. package/dist/services/api/CacheManager.d.ts +21 -0
  22. package/dist/services/api/CacheManager.d.ts.map +1 -0
  23. package/dist/services/api/CacheManager.js +25 -0
  24. package/dist/services/api/CacheManager.js.map +1 -0
  25. package/dist/services/api/comments.d.ts +8 -0
  26. package/dist/services/api/comments.d.ts.map +1 -0
  27. package/dist/services/api/comments.js +86 -0
  28. package/dist/services/api/comments.js.map +1 -0
  29. package/dist/services/api/customFields.d.ts +21 -0
  30. package/dist/services/api/customFields.d.ts.map +1 -0
  31. package/dist/services/api/customFields.js +277 -0
  32. package/dist/services/api/customFields.js.map +1 -0
  33. package/dist/services/api/index.d.ts +22 -0
  34. package/dist/services/api/index.d.ts.map +1 -0
  35. package/dist/services/api/index.js +64 -0
  36. package/dist/services/api/index.js.map +1 -0
  37. package/dist/services/api/projects.d.ts +18 -0
  38. package/dist/services/api/projects.d.ts.map +1 -0
  39. package/dist/services/api/projects.js +346 -0
  40. package/dist/services/api/projects.js.map +1 -0
  41. package/dist/services/api/recurringTasks.d.ts +15 -0
  42. package/dist/services/api/recurringTasks.d.ts.map +1 -0
  43. package/dist/services/api/recurringTasks.js +143 -0
  44. package/dist/services/api/recurringTasks.js.map +1 -0
  45. package/dist/services/api/schedules.d.ts +8 -0
  46. package/dist/services/api/schedules.d.ts.map +1 -0
  47. package/dist/services/api/schedules.js +70 -0
  48. package/dist/services/api/schedules.js.map +1 -0
  49. package/dist/services/api/search.d.ts +29 -0
  50. package/dist/services/api/search.d.ts.map +1 -0
  51. package/dist/services/api/search.js +415 -0
  52. package/dist/services/api/search.js.map +1 -0
  53. package/dist/services/api/statuses.d.ts +7 -0
  54. package/dist/services/api/statuses.d.ts.map +1 -0
  55. package/dist/services/api/statuses.js +67 -0
  56. package/dist/services/api/statuses.js.map +1 -0
  57. package/dist/services/api/tasks.d.ts +34 -0
  58. package/dist/services/api/tasks.d.ts.map +1 -0
  59. package/dist/services/api/tasks.js +474 -0
  60. package/dist/services/api/tasks.js.map +1 -0
  61. package/dist/services/api/types.d.ts +54 -0
  62. package/dist/services/api/types.d.ts.map +1 -0
  63. package/dist/services/api/types.js +9 -0
  64. package/dist/services/api/types.js.map +1 -0
  65. package/dist/services/api/users.d.ts +8 -0
  66. package/dist/services/api/users.d.ts.map +1 -0
  67. package/dist/services/api/users.js +93 -0
  68. package/dist/services/api/users.js.map +1 -0
  69. package/dist/services/api/workspaces.d.ts +7 -0
  70. package/dist/services/api/workspaces.d.ts.map +1 -0
  71. package/dist/services/api/workspaces.js +59 -0
  72. package/dist/services/api/workspaces.js.map +1 -0
  73. package/dist/services/motionApi.d.ts +34 -180
  74. package/dist/services/motionApi.d.ts.map +1 -1
  75. package/dist/services/motionApi.js +84 -2163
  76. package/dist/services/motionApi.js.map +1 -1
  77. package/dist/tools/ToolConfigurator.d.ts +1 -7
  78. package/dist/tools/ToolConfigurator.d.ts.map +1 -1
  79. package/dist/tools/ToolConfigurator.js +0 -31
  80. package/dist/tools/ToolConfigurator.js.map +1 -1
  81. package/dist/tools/ToolDefinitions.js +1 -1
  82. package/dist/tools/ToolDefinitions.js.map +1 -1
  83. package/dist/tools/ToolRegistry.d.ts +1 -22
  84. package/dist/tools/ToolRegistry.d.ts.map +1 -1
  85. package/dist/tools/ToolRegistry.js +0 -31
  86. package/dist/tools/ToolRegistry.js.map +1 -1
  87. package/dist/types/motion.d.ts +0 -7
  88. package/dist/types/motion.d.ts.map +1 -1
  89. package/dist/utils/cache.d.ts +0 -5
  90. package/dist/utils/cache.d.ts.map +1 -1
  91. package/dist/utils/cache.js +0 -8
  92. package/dist/utils/cache.js.map +1 -1
  93. package/dist/utils/constants.d.ts +4 -0
  94. package/dist/utils/constants.d.ts.map +1 -1
  95. package/dist/utils/constants.js +7 -1
  96. package/dist/utils/constants.js.map +1 -1
  97. package/dist/utils/errorHandling.d.ts +0 -12
  98. package/dist/utils/errorHandling.d.ts.map +1 -1
  99. package/dist/utils/errorHandling.js +2 -23
  100. package/dist/utils/errorHandling.js.map +1 -1
  101. package/dist/utils/errors.d.ts +89 -0
  102. package/dist/utils/errors.d.ts.map +1 -0
  103. package/dist/utils/errors.js +276 -0
  104. package/dist/utils/errors.js.map +1 -0
  105. package/dist/utils/index.d.ts +2 -2
  106. package/dist/utils/index.d.ts.map +1 -1
  107. package/dist/utils/index.js +6 -5
  108. package/dist/utils/index.js.map +1 -1
  109. package/dist/utils/parameterUtils.d.ts +10 -0
  110. package/dist/utils/parameterUtils.d.ts.map +1 -1
  111. package/dist/utils/parameterUtils.js +61 -5
  112. package/dist/utils/parameterUtils.js.map +1 -1
  113. package/dist/utils/responseFormatters.js +16 -16
  114. package/dist/utils/responseFormatters.js.map +1 -1
  115. package/dist/utils/serverInstructions.d.ts +2 -0
  116. package/dist/utils/serverInstructions.d.ts.map +1 -0
  117. package/dist/utils/serverInstructions.js +21 -0
  118. package/dist/utils/serverInstructions.js.map +1 -0
  119. package/dist/utils/userFacingErrors.d.ts +0 -8
  120. package/dist/utils/userFacingErrors.d.ts.map +1 -1
  121. package/dist/utils/userFacingErrors.js +0 -19
  122. package/dist/utils/userFacingErrors.js.map +1 -1
  123. package/dist/utils/workspaceResolver.js +12 -12
  124. package/dist/utils/workspaceResolver.js.map +1 -1
  125. package/package.json +2 -1
@@ -1,2254 +1,175 @@
1
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
- })();
2
+ /**
3
+ * MotionApiService thin facade that delegates to resource modules.
4
+ *
5
+ * All business logic lives in `src/services/api/*.ts` modules. This class
6
+ * preserves the original public API surface so existing consumers (handlers,
7
+ * tests) don't need to change.
8
+ */
35
9
  Object.defineProperty(exports, "__esModule", { value: true });
36
10
  exports.MotionApiService = void 0;
37
- const axios_1 = __importStar(require("axios"));
38
- const constants_1 = require("../utils/constants");
39
- const frequencyTransform_1 = require("../utils/frequencyTransform");
40
- const logger_1 = require("../utils/logger");
41
- const cache_1 = require("../utils/cache");
42
- const paginationNew_1 = require("../utils/paginationNew");
43
- const responseWrapper_1 = require("../utils/responseWrapper");
44
- const userFacingErrors_1 = require("../utils/userFacingErrors");
45
- const zod_1 = require("zod");
46
- const motion_1 = require("../schemas/motion");
47
- // Note: Using native axios.isAxiosError instead of custom implementation
48
- // Helper to get error message
49
- function getErrorMessage(error) {
50
- if (error instanceof Error) {
51
- return error.message;
52
- }
53
- return String(error);
54
- }
11
+ const ApiClient_1 = require("./api/ApiClient");
12
+ const CacheManager_1 = require("./api/CacheManager");
13
+ // Resource module imports
14
+ const workspaces_1 = require("./api/workspaces");
15
+ const users_1 = require("./api/users");
16
+ const tasks_1 = require("./api/tasks");
17
+ const projects_1 = require("./api/projects");
18
+ const comments_1 = require("./api/comments");
19
+ const customFields_1 = require("./api/customFields");
20
+ const recurringTasks_1 = require("./api/recurringTasks");
21
+ const schedules_1 = require("./api/schedules");
22
+ const statuses_1 = require("./api/statuses");
23
+ const search_1 = require("./api/search");
55
24
  class MotionApiService {
56
- /**
57
- * Validate API response against schema
58
- * Handles strict/lenient/off modes based on configuration
59
- */
60
- validateResponse(data, schema, context) {
61
- if (motion_1.VALIDATION_CONFIG.mode === 'off') {
62
- return data;
63
- }
64
- try {
65
- return schema.parse(data);
66
- }
67
- catch (error) {
68
- if (error instanceof zod_1.z.ZodError) {
69
- const errorDetails = {
70
- context,
71
- validationErrors: error.issues,
72
- ...(motion_1.VALIDATION_CONFIG.includeDataInLogs ? { receivedData: data } : {})
73
- };
74
- if (motion_1.VALIDATION_CONFIG.logErrors) {
75
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, `API response validation failed for ${context}`, errorDetails);
76
- }
77
- if (motion_1.VALIDATION_CONFIG.mode === 'strict') {
78
- throw new Error(`Invalid API response structure for ${context}: ${error.message}`);
79
- }
80
- // Lenient mode: return original data and hope for the best
81
- return data;
82
- }
83
- throw error;
84
- }
85
- }
86
25
  constructor(apiKey) {
87
- const resolvedApiKey = apiKey || process.env.MOTION_API_KEY;
88
- if (!resolvedApiKey) {
89
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Motion API key not found in environment variables', {
90
- component: 'MotionApiService',
91
- method: 'constructor'
92
- });
93
- throw new Error('MOTION_API_KEY environment variable is required');
94
- }
95
- this.apiKey = resolvedApiKey;
96
- this.baseUrl = 'https://api.usemotion.com/v1';
97
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Initializing Motion API service', {
98
- component: 'MotionApiService',
99
- baseUrl: this.baseUrl
100
- });
101
- this.client = axios_1.default.create({
102
- baseURL: this.baseUrl,
103
- headers: {
104
- 'X-API-Key': this.apiKey,
105
- 'Content-Type': 'application/json'
106
- },
107
- timeout: constants_1.API_CONFIG.TIMEOUT_MS
108
- });
109
- // Initialize cache instances with TTL from constants (converted to ms)
110
- this.workspaceCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.WORKSPACES);
111
- this.userCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.USERS);
112
- this.projectCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.PROJECTS);
113
- this.singleProjectCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.PROJECTS);
114
- this.commentCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.COMMENTS);
115
- this.customFieldCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.CUSTOM_FIELDS);
116
- this.recurringTaskCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.RECURRING_TASKS);
117
- this.scheduleCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.SCHEDULES);
118
- this.statusCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.WORKSPACES); // 10 minutes, like workspaces
119
- this.client.interceptors.response.use((response) => {
120
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Motion API response successful', {
121
- url: response.config?.url,
122
- method: response.config?.method?.toUpperCase(),
123
- status: response.status,
124
- component: 'MotionApiService'
125
- });
126
- return response;
127
- }, (error) => {
128
- const errorData = error.response?.data;
129
- const errorDetails = {
130
- url: error.config?.url,
131
- method: error.config?.method?.toUpperCase(),
132
- status: error.response?.status,
133
- statusText: error.response?.statusText,
134
- apiMessage: errorData?.message,
135
- apiCode: errorData?.code,
136
- errorMessage: error.message,
137
- component: 'MotionApiService'
138
- };
139
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Motion API request failed', errorDetails);
140
- // Re-throw original AxiosError so requestWithRetry can detect it
141
- // via axios.isAxiosError() for retry/rate-limit handling
142
- return Promise.reject(error);
143
- });
26
+ this._api = new ApiClient_1.ApiClient(apiKey);
27
+ this._cache = new CacheManager_1.CacheManager();
144
28
  }
145
- /**
146
- * Formats API errors with user-friendly messages while preserving technical details
147
- * @param error - The error that occurred
148
- * @param action - Description of the action that failed (e.g., 'fetch', 'create', 'update')
149
- * @param resourceType - Type of resource being operated on (e.g., 'project', 'task')
150
- * @param resourceId - Optional ID of the resource
151
- * @param resourceName - Optional name of the resource
152
- * @returns UserFacingError with both user-friendly and technical messages
153
- */
154
- formatApiError(error, action, resourceType, resourceId, resourceName) {
155
- const context = (0, userFacingErrors_1.createErrorContext)(action, resourceType, resourceId, resourceName);
156
- return (0, userFacingErrors_1.createUserFacingError)(error, context);
29
+ /** ResourceContext for delegating to extracted resource modules. */
30
+ get _ctx() {
31
+ return { api: this._api, cache: this._cache };
157
32
  }
158
33
  // ========================================
159
- // PRIVATE HELPER METHODS
34
+ // WORKSPACE API METHODS
160
35
  // ========================================
161
- /**
162
- * Wraps an axios request with a retry mechanism featuring exponential backoff.
163
- * Retries only safe/read-only requests (GET) on transient failures:
164
- * - HTTP 5xx
165
- * - HTTP 429
166
- * - Network errors with no HTTP response
167
- */
168
- async requestWithRetry(request) {
169
- for (let attempt = 1; attempt <= constants_1.RETRY_CONFIG.MAX_RETRIES; attempt++) {
170
- try {
171
- return await request();
172
- }
173
- catch (error) {
174
- if (!axios_1.default.isAxiosError(error)) {
175
- throw error; // Not a network error, re-throw immediately
176
- }
177
- const status = error.response?.status;
178
- const requestMethod = error.config?.method?.toUpperCase() || 'GET';
179
- const isSafeMethod = requestMethod === 'GET';
180
- const isNetworkError = !error.response;
181
- const isRetryableStatus = status === 429 || (status !== undefined && status >= 500);
182
- const isRetryable = isSafeMethod && (isNetworkError || isRetryableStatus);
183
- if (!isRetryable || attempt === constants_1.RETRY_CONFIG.MAX_RETRIES) {
184
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, `Request failed and will not be retried`, {
185
- status,
186
- httpMethod: requestMethod,
187
- attempt,
188
- maxRetries: constants_1.RETRY_CONFIG.MAX_RETRIES,
189
- isRetryable,
190
- component: 'MotionApiService',
191
- method: 'requestWithRetry'
192
- });
193
- throw error; // Final attempt failed or error is not retryable
194
- }
195
- // Handle Retry-After header for 429
196
- const retryAfterHeaderRaw = error.response?.headers['retry-after'];
197
- const retryAfterHeader = Array.isArray(retryAfterHeaderRaw) ? retryAfterHeaderRaw[0] : retryAfterHeaderRaw;
198
- let delay = 0;
199
- if (status === 429 && retryAfterHeader) {
200
- const retryAfterSeconds = Number(retryAfterHeader);
201
- if (!isNaN(retryAfterSeconds) && retryAfterSeconds >= 0) {
202
- delay = retryAfterSeconds * 1000;
203
- }
204
- else {
205
- const retryAfterDate = Date.parse(retryAfterHeader);
206
- if (!isNaN(retryAfterDate)) {
207
- delay = Math.max(0, retryAfterDate - Date.now());
208
- }
209
- }
210
- }
211
- // If no Retry-After header, use exponential backoff with jitter
212
- if (delay === 0) {
213
- const backoff = constants_1.RETRY_CONFIG.INITIAL_BACKOFF_MS * Math.pow(constants_1.RETRY_CONFIG.BACKOFF_MULTIPLIER, attempt - 1);
214
- const jitter = backoff * constants_1.RETRY_CONFIG.JITTER_FACTOR * Math.random();
215
- delay = Math.min(backoff + jitter, constants_1.RETRY_CONFIG.MAX_BACKOFF_MS);
216
- }
217
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, `Request failed, retrying`, {
218
- attempt,
219
- maxRetries: constants_1.RETRY_CONFIG.MAX_RETRIES,
220
- delayMs: Math.round(delay),
221
- error: error.message,
222
- status,
223
- httpMethod: requestMethod,
224
- isNetworkError,
225
- component: 'MotionApiService',
226
- method: 'requestWithRetry'
227
- });
228
- await new Promise(resolve => setTimeout(resolve, delay));
229
- }
230
- }
231
- // Should never reach here, but TypeScript requires a return or throw
232
- throw new Error('Max retries exceeded');
233
- }
234
- /**
235
- * Merge truncation metadata from multiple paginated sources without mutating source objects.
236
- * If sources disagree on reason/limit, drop those fields to avoid misleading aggregate metadata.
237
- */
238
- mergeTruncationMetadata(aggregate, source) {
239
- if (!source?.wasTruncated) {
240
- return aggregate;
241
- }
242
- if (!aggregate?.wasTruncated) {
243
- return { ...source };
244
- }
245
- const merged = { ...aggregate, wasTruncated: true };
246
- if (aggregate.reason !== source.reason) {
247
- delete merged.reason;
248
- delete merged.limit;
249
- return merged;
250
- }
251
- if (source.reason !== undefined) {
252
- merged.reason = source.reason;
253
- }
254
- if (aggregate.limit !== source.limit) {
255
- delete merged.limit;
256
- }
257
- else if (source.limit !== undefined) {
258
- merged.limit = source.limit;
259
- }
260
- return merged;
36
+ async getWorkspaces(ids) {
37
+ return (0, workspaces_1.getWorkspaces)(this._ctx, ids);
261
38
  }
262
39
  // ========================================
263
- // PROJECT API METHODS
40
+ // USER API METHODS
264
41
  // ========================================
265
- async getProjects(workspaceId, options) {
266
- const { maxPages = 5, limit } = options || {};
267
- // Validate limit parameter if provided
268
- if (limit !== undefined && (limit < 0 || !Number.isInteger(limit))) {
269
- throw new Error('limit must be a non-negative integer');
270
- }
271
- const cacheKey = `projects:workspace:${workspaceId}:maxPages:${maxPages ?? 'default'}:limit:${limit ?? 'none'}`;
272
- // Check cache - return items only (no stale truncation info)
273
- const cachedItems = this.projectCache.get(cacheKey);
274
- if (cachedItems !== null) {
275
- return { items: cachedItems };
276
- }
277
- try {
278
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from Motion API', {
279
- method: 'getProjects',
280
- workspaceId,
281
- maxPages,
282
- limit
283
- });
284
- // Create a fetch function for potential pagination
285
- const fetchPage = async (cursor) => {
286
- const params = new URLSearchParams();
287
- params.append('workspaceId', workspaceId);
288
- if (cursor) {
289
- params.append('cursor', cursor);
290
- }
291
- const queryString = params.toString();
292
- const url = `/projects?${queryString}`;
293
- return this.requestWithRetry(() => this.client.get(url));
294
- };
295
- try {
296
- // Attempt pagination-aware fetch with new response wrapper
297
- const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'projects', {
298
- maxPages,
299
- logProgress: false,
300
- ...(limit ? { maxItems: limit } : {})
301
- });
302
- const projects = paginatedResult.items;
303
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully with pagination', {
304
- method: 'getProjects',
305
- totalCount: projects.length,
306
- hasMore: paginatedResult.hasMore,
307
- workspaceId
308
- });
309
- // Cache only items, not truncation metadata
310
- this.projectCache.set(cacheKey, projects);
311
- return { items: projects, truncation: paginatedResult.truncation };
312
- }
313
- catch (paginationError) {
314
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
315
- method: 'getProjects',
316
- error: paginationError instanceof Error ? paginationError.message : String(paginationError)
317
- });
318
- }
319
- // Use new response wrapper for single page fallback
320
- const response = await fetchPage();
321
- const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'projects');
322
- let projects = unwrapped.data;
323
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully (single page)', {
324
- method: 'getProjects',
325
- count: projects.length,
326
- workspaceId
327
- });
328
- // Do not cache fallback results. If pagination failed, this may be only the first page.
329
- return { items: projects };
330
- }
331
- catch (error) {
332
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects', {
333
- method: 'getProjects',
334
- error: getErrorMessage(error),
335
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
336
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
337
- });
338
- throw this.formatApiError(error, 'fetch', 'project');
339
- }
340
- }
341
- async getAllProjects() {
342
- try {
343
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from all workspaces', {
344
- method: 'getAllProjects'
345
- });
346
- const allWorkspaces = await this.getWorkspaces();
347
- const allProjects = [];
348
- let aggregateTruncation;
349
- for (const workspace of allWorkspaces) {
350
- try {
351
- const { items, truncation } = await this.getProjects(workspace.id);
352
- allProjects.push(...items);
353
- aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, truncation);
354
- }
355
- catch (workspaceError) {
356
- // Log error but continue with other workspaces
357
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to fetch projects from workspace', {
358
- method: 'getAllProjects',
359
- workspaceId: workspace.id,
360
- workspaceName: workspace.name,
361
- error: getErrorMessage(workspaceError)
362
- });
363
- }
364
- }
365
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'All projects fetched successfully', {
366
- method: 'getAllProjects',
367
- totalProjects: allProjects.length,
368
- workspaceCount: allWorkspaces.length
369
- });
370
- if (aggregateTruncation) {
371
- aggregateTruncation.returnedCount = allProjects.length;
372
- }
373
- return { items: allProjects, truncation: aggregateTruncation };
374
- }
375
- catch (error) {
376
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects from all workspaces', {
377
- method: 'getAllProjects',
378
- error: getErrorMessage(error)
379
- });
380
- throw this.formatApiError(error, 'fetch', 'project');
381
- }
382
- }
383
- async getProject(projectId) {
384
- const cacheKey = `project:${projectId}`;
385
- return this.singleProjectCache.withCache(cacheKey, async () => {
386
- try {
387
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching single project from Motion API', {
388
- method: 'getProject',
389
- projectId
390
- });
391
- const response = await this.requestWithRetry(() => this.client.get(`/projects/${projectId}`));
392
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Successfully fetched project', {
393
- method: 'getProject',
394
- projectId,
395
- projectName: response.data.name
396
- });
397
- return response.data;
398
- }
399
- catch (error) {
400
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch project', {
401
- method: 'getProject',
402
- projectId,
403
- error: getErrorMessage(error),
404
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
405
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
406
- });
407
- throw this.formatApiError(error, 'fetch', 'project', projectId);
408
- }
409
- });
410
- }
411
- async createProject(projectData) {
412
- try {
413
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating project in Motion API', {
414
- method: 'createProject',
415
- projectName: projectData.name,
416
- workspaceId: projectData.workspaceId
417
- });
418
- if (!projectData.workspaceId) {
419
- throw new Error('Workspace ID is required to create a project');
420
- }
421
- // Create minimal payload by removing empty/null values to avoid validation errors
422
- const minimalPayload = (0, constants_1.createMinimalPayload)(projectData);
423
- // Debug logging: log the exact payload being sent to API
424
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'API payload for project creation', {
425
- method: 'createProject',
426
- payload: JSON.stringify(minimalPayload, null, 2)
427
- });
428
- const response = await this.requestWithRetry(() => this.client.post('/projects', minimalPayload));
429
- // Invalidate cache after successful creation
430
- this.projectCache.invalidate(`projects:workspace:${projectData.workspaceId}`);
431
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project created successfully', {
432
- method: 'createProject',
433
- projectId: response.data.id,
434
- projectName: response.data.name
435
- });
436
- return response.data;
437
- }
438
- catch (error) {
439
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create project', {
440
- method: 'createProject',
441
- error: getErrorMessage(error),
442
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
443
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
444
- fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
445
- });
446
- throw this.formatApiError(error, 'create', 'project', undefined, projectData.name);
447
- }
448
- }
449
- // Note: Project update/delete are not in the public API docs but appear to be functional
450
- async updateProject(projectId, updates) {
451
- try {
452
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Updating project in Motion API', {
453
- method: 'updateProject',
454
- projectId,
455
- updates: Object.keys(updates)
456
- });
457
- // Create minimal payload by removing empty/null values to avoid validation errors
458
- const minimalUpdates = (0, constants_1.createMinimalPayload)(updates);
459
- const response = await this.requestWithRetry(() => this.client.patch(`/projects/${projectId}`, minimalUpdates));
460
- // Invalidate cache after successful update
461
- this.projectCache.invalidate(`projects:workspace:${response.data.workspaceId}`);
462
- this.singleProjectCache.invalidate(`project:${projectId}`);
463
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project updated successfully', {
464
- method: 'updateProject',
465
- projectId,
466
- projectName: response.data.name
467
- });
468
- return response.data;
469
- }
470
- catch (error) {
471
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to update project', {
472
- method: 'updateProject',
473
- projectId,
474
- error: getErrorMessage(error),
475
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
476
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
477
- });
478
- throw this.formatApiError(error, 'update', 'project', projectId);
479
- }
42
+ async getUsers(workspaceId, teamId) {
43
+ return (0, users_1.getUsers)(this._ctx, workspaceId, teamId);
480
44
  }
481
- // Note: Project delete is not in the public API docs but appears to be functional
482
- async deleteProject(projectId) {
483
- try {
484
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting project from Motion API', {
485
- method: 'deleteProject',
486
- projectId
487
- });
488
- await this.requestWithRetry(() => this.client.delete(`/projects/${projectId}`));
489
- // Invalidate all project caches since we don't know the workspace ID
490
- this.projectCache.invalidate();
491
- this.singleProjectCache.invalidate(`project:${projectId}`);
492
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project deleted successfully', {
493
- method: 'deleteProject',
494
- projectId
495
- });
496
- }
497
- catch (error) {
498
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete project', {
499
- method: 'deleteProject',
500
- projectId,
501
- error: getErrorMessage(error),
502
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
503
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
504
- });
505
- throw this.formatApiError(error, 'delete', 'project', projectId);
506
- }
45
+ async getCurrentUser() {
46
+ return (0, users_1.getCurrentUser)(this._ctx);
507
47
  }
508
48
  // ========================================
509
49
  // TASK API METHODS
510
50
  // ========================================
511
51
  async getTasks(options) {
512
- const { workspaceId, projectId, name, status, includeAllStatuses, assigneeId, priority, dueDate, labels, limit, maxPages = 5 } = options;
513
- // Validate limit parameter if provided
514
- if (limit !== undefined && (limit < 0 || !Number.isInteger(limit))) {
515
- throw new Error('limit must be a non-negative integer');
516
- }
517
- try {
518
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching tasks from Motion API', {
519
- method: 'getTasks',
520
- workspaceId,
521
- projectId,
522
- status,
523
- includeAllStatuses,
524
- assigneeId,
525
- priority,
526
- dueDate,
527
- labelsCount: labels?.length,
528
- maxPages
529
- });
530
- // Client-side filters for params not supported by the API
531
- const applyClientFilters = (tasks) => {
532
- let filtered = tasks;
533
- if (priority) {
534
- filtered = filtered.filter(t => t.priority === priority);
535
- }
536
- if (dueDate) {
537
- // Compare date portion only (YYYY-MM-DD)
538
- filtered = filtered.filter(t => {
539
- if (!t.dueDate)
540
- return false;
541
- const taskDate = t.dueDate.substring(0, 10);
542
- return taskDate <= dueDate;
543
- });
544
- }
545
- return filtered;
546
- };
547
- // Create a fetch function for potential pagination
548
- const fetchPage = async (cursor) => {
549
- const params = new URLSearchParams();
550
- if (workspaceId) {
551
- params.append('workspaceId', workspaceId);
552
- }
553
- if (projectId) {
554
- params.append('projectId', projectId);
555
- }
556
- if (status) {
557
- if (Array.isArray(status)) {
558
- // Deduplicate and skip empty strings before appending
559
- // Note: Motion API supports repeated status= params for multi-value filtering
560
- const uniqueStatuses = Array.from(new Set(status));
561
- for (const s of uniqueStatuses) {
562
- if (s) {
563
- params.append('status', s);
564
- }
565
- }
566
- }
567
- else {
568
- params.append('status', status);
569
- }
570
- }
571
- if (includeAllStatuses) {
572
- params.append('includeAllStatuses', 'true');
573
- }
574
- if (assigneeId) {
575
- params.append('assigneeId', assigneeId);
576
- }
577
- // Note: priority and dueDate are NOT valid API query params — filtered client-side after fetch
578
- if (name) {
579
- params.append('name', name);
580
- }
581
- if (labels && labels.length > 0) {
582
- // API accepts 'label' (singular) as a string parameter
583
- for (const label of labels) {
584
- if (label) {
585
- params.append('label', label);
586
- }
587
- }
588
- }
589
- if (cursor) {
590
- params.append('cursor', cursor);
591
- }
592
- const queryString = params.toString();
593
- const url = queryString ? `/tasks?${queryString}` : '/tasks';
594
- return this.requestWithRetry(() => this.client.get(url));
595
- };
596
- try {
597
- // When client-side filters are active, don't cap pagination with maxItems
598
- // because valid matches may exist beyond the first batch. Fetch all pages
599
- // and apply the limit after filtering instead.
600
- const hasClientFilters = Boolean(priority || dueDate);
601
- const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'tasks', {
602
- maxPages,
603
- logProgress: false, // Less verbose for tasks
604
- ...(!hasClientFilters && limit ? { maxItems: limit } : {})
605
- });
606
- let filteredItems = applyClientFilters(paginatedResult.items);
607
- let truncation = paginatedResult.truncation;
608
- // When client-side filters reduced the result set and pagination was also truncated,
609
- // update the truncation info so the notice accurately reflects what the user sees
610
- if (hasClientFilters && filteredItems.length < paginatedResult.items.length && truncation?.wasTruncated) {
611
- truncation = {
612
- ...truncation,
613
- clientFiltered: true,
614
- fetchedCount: paginatedResult.items.length,
615
- returnedCount: filteredItems.length,
616
- };
617
- }
618
- // Apply limit after client-side filtering
619
- if (hasClientFilters && limit && limit > 0 && filteredItems.length > limit) {
620
- truncation = {
621
- wasTruncated: true,
622
- returnedCount: limit,
623
- reason: 'max_items',
624
- limit,
625
- };
626
- filteredItems = filteredItems.slice(0, limit);
627
- }
628
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully with pagination', {
629
- method: 'getTasks',
630
- totalCount: paginatedResult.totalFetched,
631
- returnedCount: filteredItems.length,
632
- clientFiltered: filteredItems.length !== paginatedResult.items.length,
633
- hasMore: paginatedResult.hasMore,
634
- workspaceId,
635
- projectId,
636
- limitApplied: limit
637
- });
638
- return { items: filteredItems, truncation };
639
- }
640
- catch (paginationError) {
641
- // Fallback to simple fetch if pagination fails
642
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
643
- method: 'getTasks',
644
- error: paginationError instanceof Error ? paginationError.message : String(paginationError)
645
- });
646
- }
647
- // Use new response wrapper for single page fallback
648
- const response = await fetchPage();
649
- const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'tasks');
650
- let tasks = applyClientFilters(unwrapped.data);
651
- // Apply limit if specified
652
- if (limit && limit > 0) {
653
- tasks = tasks.slice(0, limit);
654
- }
655
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully (single page)', {
656
- method: 'getTasks',
657
- count: tasks.length,
658
- workspaceId,
659
- projectId,
660
- limitApplied: limit
661
- });
662
- return { items: tasks };
663
- }
664
- catch (error) {
665
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch tasks', {
666
- method: 'getTasks',
667
- error: getErrorMessage(error),
668
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
669
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
670
- });
671
- throw this.formatApiError(error, 'fetch', 'task');
672
- }
52
+ return (0, tasks_1.getTasks)(this._ctx, options);
673
53
  }
674
54
  async getTask(taskId) {
675
- try {
676
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching single task from Motion API', {
677
- method: 'getTask',
678
- taskId
679
- });
680
- const response = await this.requestWithRetry(() => this.client.get(`/tasks/${taskId}`));
681
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Successfully fetched task', {
682
- method: 'getTask',
683
- taskId,
684
- taskName: response.data.name
685
- });
686
- return response.data;
687
- }
688
- catch (error) {
689
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch task', {
690
- method: 'getTask',
691
- taskId,
692
- error: getErrorMessage(error),
693
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
694
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
695
- });
696
- throw this.formatApiError(error, 'fetch', 'task', taskId);
697
- }
55
+ return (0, tasks_1.getTask)(this._ctx, taskId);
698
56
  }
699
57
  async createTask(taskData) {
700
- try {
701
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating task in Motion API', {
702
- method: 'createTask',
703
- taskName: taskData.name,
704
- workspaceId: taskData.workspaceId,
705
- projectId: taskData.projectId
706
- });
707
- if (!taskData.workspaceId) {
708
- throw new Error('Workspace ID is required to create a task');
709
- }
710
- // Create minimal payload by removing empty/null values to avoid validation errors
711
- const minimalPayload = (0, constants_1.createMinimalPayload)(taskData);
712
- // Debug logging: log the exact payload being sent to API
713
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'API payload for task creation', {
714
- method: 'createTask',
715
- payload: JSON.stringify(minimalPayload, null, 2)
716
- });
717
- const response = await this.requestWithRetry(() => this.client.post('/tasks', minimalPayload));
718
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task created successfully', {
719
- method: 'createTask',
720
- taskId: response.data.id,
721
- taskName: response.data.name
722
- });
723
- return response.data;
724
- }
725
- catch (error) {
726
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create task', {
727
- method: 'createTask',
728
- error: getErrorMessage(error),
729
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
730
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
731
- fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
732
- });
733
- throw this.formatApiError(error, 'create', 'task', undefined, taskData.name);
734
- }
58
+ return (0, tasks_1.createTask)(this._ctx, taskData);
735
59
  }
736
- // Note: API docs list name and workspaceId as required for PATCH /tasks/{id},
737
- // but the API appears to accept partial updates without them. Not enforced here.
738
60
  async updateTask(taskId, updates) {
739
- try {
740
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Updating task in Motion API', {
741
- method: 'updateTask',
742
- taskId,
743
- updates: Object.keys(updates)
744
- });
745
- // Create minimal payload by removing empty/null values to avoid validation errors
746
- const minimalUpdates = (0, constants_1.createMinimalPayload)(updates);
747
- const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}`, minimalUpdates));
748
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task updated successfully', {
749
- method: 'updateTask',
750
- taskId,
751
- taskName: response.data.name
752
- });
753
- return response.data;
754
- }
755
- catch (error) {
756
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to update task', {
757
- method: 'updateTask',
758
- taskId,
759
- error: getErrorMessage(error),
760
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
761
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
762
- });
763
- throw this.formatApiError(error, 'update', 'task', taskId);
764
- }
61
+ return (0, tasks_1.updateTask)(this._ctx, taskId, updates);
765
62
  }
766
63
  async deleteTask(taskId) {
767
- try {
768
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting task from Motion API', {
769
- method: 'deleteTask',
770
- taskId
771
- });
772
- await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}`));
773
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task deleted successfully', {
774
- method: 'deleteTask',
775
- taskId
776
- });
777
- }
778
- catch (error) {
779
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete task', {
780
- method: 'deleteTask',
781
- taskId,
782
- error: getErrorMessage(error),
783
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
784
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
785
- });
786
- throw this.formatApiError(error, 'delete', 'task', taskId);
787
- }
64
+ return (0, tasks_1.deleteTask)(this._ctx, taskId);
788
65
  }
789
66
  async moveTask(taskId, targetWorkspaceId, assigneeId) {
790
- try {
791
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Moving task in Motion API', {
792
- method: 'moveTask',
793
- taskId,
794
- targetWorkspaceId,
795
- assigneeId
796
- });
797
- // API requires workspaceId, optionally accepts assigneeId
798
- const moveData = {
799
- workspaceId: targetWorkspaceId,
800
- };
801
- if (assigneeId !== undefined)
802
- moveData.assigneeId = assigneeId;
803
- const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}/move`, moveData));
804
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task moved successfully', {
805
- method: 'moveTask',
806
- taskId,
807
- targetWorkspaceId,
808
- assigneeId
809
- });
810
- // Docs say 200 with task object, but handle 204 No Content defensively
811
- return response.status === 204 ? undefined : response.data;
812
- }
813
- catch (error) {
814
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to move task', {
815
- method: 'moveTask',
816
- taskId,
817
- targetWorkspaceId,
818
- assigneeId,
819
- error: getErrorMessage(error),
820
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
821
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
822
- });
823
- throw this.formatApiError(error, 'move', 'task', taskId);
824
- }
67
+ return (0, tasks_1.moveTask)(this._ctx, taskId, targetWorkspaceId, assigneeId);
825
68
  }
826
69
  async unassignTask(taskId) {
827
- try {
828
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Unassigning task in Motion API', {
829
- method: 'unassignTask',
830
- taskId
831
- });
832
- const response = await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}/assignee`));
833
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task unassigned successfully', {
834
- method: 'unassignTask',
835
- taskId
836
- });
837
- // Response undocumented — handle 204 No Content defensively
838
- return response.status === 204 ? undefined : response.data;
839
- }
840
- catch (error) {
841
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to unassign task', {
842
- method: 'unassignTask',
843
- taskId,
844
- error: getErrorMessage(error),
845
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
846
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
847
- });
848
- throw this.formatApiError(error, 'unassign', 'task', taskId);
849
- }
70
+ return (0, tasks_1.unassignTask)(this._ctx, taskId);
850
71
  }
851
- // ========================================
852
- // WORKSPACE API METHODS
853
- // ========================================
854
- async getWorkspaces(ids) {
855
- // Skip caching when filtering by IDs — filtered results are unlikely to be reused
856
- if (ids && ids.length > 0) {
857
- return this.fetchWorkspaces(ids);
858
- }
859
- return this.workspaceCache.withCache('workspaces', async () => {
860
- return this.fetchWorkspaces();
861
- });
862
- }
863
- async fetchWorkspaces(ids) {
864
- try {
865
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching workspaces from Motion API', {
866
- method: 'getWorkspaces',
867
- filterIds: ids
868
- });
869
- const params = new URLSearchParams();
870
- if (ids && ids.length > 0) {
871
- for (const id of ids) {
872
- params.append('ids', id);
873
- }
874
- }
875
- const queryString = params.toString();
876
- const url = queryString ? `/workspaces?${queryString}` : '/workspaces';
877
- const response = await this.requestWithRetry(() => this.client.get(url));
878
- // Validate the response structure
879
- const validatedResponse = this.validateResponse(response.data, motion_1.WorkspacesListResponseSchema, 'getWorkspaces');
880
- // Extract workspaces array (handle both wrapped and unwrapped responses)
881
- const workspaces = Array.isArray(validatedResponse)
882
- ? validatedResponse
883
- : validatedResponse.workspaces;
884
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Workspaces fetched successfully', {
885
- method: 'getWorkspaces',
886
- count: workspaces.length,
887
- workspaceNames: workspaces.map((w) => w.name)
888
- });
889
- return workspaces;
890
- }
891
- catch (error) {
892
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch workspaces', {
893
- method: 'getWorkspaces',
894
- error: getErrorMessage(error),
895
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
896
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
897
- });
898
- throw this.formatApiError(error, 'fetch', 'workspace');
899
- }
72
+ async getAllUncompletedTasks(limit, assigneeId) {
73
+ return (0, tasks_1.getAllUncompletedTasks)(this._ctx, limit, assigneeId);
900
74
  }
901
75
  // ========================================
902
- // USER API METHODS
76
+ // PROJECT API METHODS
903
77
  // ========================================
904
- async getUsers(workspaceId, teamId) {
905
- const parts = [workspaceId && `ws:${workspaceId}`, teamId && `team:${teamId}`].filter(Boolean);
906
- const cacheKey = parts.length > 0 ? `users:${parts.join(':')}` : 'users:all';
907
- return this.userCache.withCache(cacheKey, async () => {
908
- try {
909
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching users from Motion API', {
910
- method: 'getUsers',
911
- workspaceId,
912
- teamId
913
- });
914
- const params = new URLSearchParams();
915
- if (workspaceId) {
916
- params.append('workspaceId', workspaceId);
917
- }
918
- if (teamId) {
919
- params.append('teamId', teamId);
920
- }
921
- const queryString = params.toString();
922
- const url = queryString ? `/users?${queryString}` : '/users';
923
- const response = await this.requestWithRetry(() => this.client.get(url));
924
- // The Motion API might wrap the users in a 'users' array
925
- const usersData = response.data?.users || response.data || [];
926
- const users = Array.isArray(usersData) ? usersData : [];
927
- if (!Array.isArray(response.data?.users) && !Array.isArray(response.data)) {
928
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Unexpected users response shape', {
929
- method: 'getUsers',
930
- workspaceId,
931
- responseType: response.data === null ? 'null' : typeof response.data
932
- });
933
- }
934
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Users fetched successfully', {
935
- method: 'getUsers',
936
- count: users.length,
937
- workspaceId
938
- });
939
- return users;
940
- }
941
- catch (error) {
942
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch users', {
943
- method: 'getUsers',
944
- error: getErrorMessage(error),
945
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
946
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
947
- });
948
- throw this.formatApiError(error, 'fetch', 'user');
949
- }
950
- });
78
+ async getProjects(workspaceId, options) {
79
+ return (0, projects_1.getProjects)(this._ctx, workspaceId, options);
951
80
  }
952
- async getCurrentUser() {
953
- const cacheKey = 'currentUser';
954
- // Use userCache but with a special single-user wrapper
955
- const cachedUsers = await this.userCache.withCache(cacheKey, async () => {
956
- try {
957
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching current user from Motion API', {
958
- method: 'getCurrentUser'
959
- });
960
- const response = await this.requestWithRetry(() => this.client.get('/users/me'));
961
- const user = response.data;
962
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Current user fetched successfully', {
963
- method: 'getCurrentUser',
964
- userId: user.id,
965
- email: user.email
966
- });
967
- return [user]; // Wrap in array for cache compatibility
968
- }
969
- catch (error) {
970
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch current user', {
971
- method: 'getCurrentUser',
972
- error: getErrorMessage(error),
973
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
974
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
975
- });
976
- throw this.formatApiError(error, 'fetch', 'user');
977
- }
978
- });
979
- const user = cachedUsers[0];
980
- if (!user) {
981
- throw this.formatApiError(new Error('No user returned from API'), 'fetch', 'user');
982
- }
983
- return user;
81
+ async getAllProjects() {
82
+ return (0, projects_1.getAllProjects)(this._ctx);
984
83
  }
985
- // ========================================
986
- // SEARCH AND RESOLUTION METHODS
987
- // ========================================
988
- /**
989
- * Resolves a project identifier (either projectId or projectName) to a MotionProject
990
- * Searches across all workspaces if not found in the specified workspace
991
- * @param identifier Object containing either projectId or projectName
992
- * @param workspaceId Workspace to start searching in
993
- * @returns Resolved MotionProject with workspace info, or undefined if not found
994
- */
995
- async resolveProjectIdentifier(identifier, workspaceId) {
996
- try {
997
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Resolving project identifier', {
998
- method: 'resolveProjectIdentifier',
999
- projectId: identifier.projectId,
1000
- projectName: identifier.projectName,
1001
- workspaceId
1002
- });
1003
- // If projectId is provided, try to get it directly
1004
- if (identifier.projectId) {
1005
- try {
1006
- const project = await this.getProject(identifier.projectId);
1007
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project resolved by ID', {
1008
- method: 'resolveProjectIdentifier',
1009
- projectId: identifier.projectId,
1010
- projectName: project.name,
1011
- workspaceId: project.workspaceId
1012
- });
1013
- return project;
1014
- }
1015
- catch (error) {
1016
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve project by ID', {
1017
- method: 'resolveProjectIdentifier',
1018
- projectId: identifier.projectId,
1019
- error: getErrorMessage(error)
1020
- });
1021
- // Fall through to projectName resolution if projectId fails
1022
- }
1023
- }
1024
- // If projectName is provided (or projectId failed), resolve by name across workspaces
1025
- if (identifier.projectName) {
1026
- const project = await this.getProjectByName(identifier.projectName, workspaceId);
1027
- if (project) {
1028
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project resolved by name across workspaces', {
1029
- method: 'resolveProjectIdentifier',
1030
- projectName: identifier.projectName,
1031
- projectId: project.id,
1032
- foundInWorkspaceId: project.workspaceId
1033
- });
1034
- return project;
1035
- }
1036
- }
1037
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve project identifier', {
1038
- method: 'resolveProjectIdentifier',
1039
- projectId: identifier.projectId,
1040
- projectName: identifier.projectName,
1041
- workspaceId
1042
- });
1043
- return undefined;
1044
- }
1045
- catch (error) {
1046
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Error resolving project identifier', {
1047
- method: 'resolveProjectIdentifier',
1048
- projectId: identifier.projectId,
1049
- projectName: identifier.projectName,
1050
- error: getErrorMessage(error)
1051
- });
1052
- throw error;
1053
- }
84
+ async getProject(projectId) {
85
+ return (0, projects_1.getProject)(this._ctx, projectId);
1054
86
  }
1055
- /**
1056
- * Resolves a user identifier (either userId or userName/email) to a MotionUser
1057
- * Searches across all workspaces if not found in the specified workspace unless strictWorkspace is true
1058
- * @param identifier Object containing either userId or userName
1059
- * @param workspaceId Workspace to start searching in (optional)
1060
- * @param options Resolution behavior options
1061
- * @returns Resolved MotionUser with workspace info, or undefined if not found
1062
- */
1063
- async resolveUserIdentifier(identifier, workspaceId, options) {
1064
- try {
1065
- const strictWorkspace = options?.strictWorkspace === true;
1066
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Resolving user identifier', {
1067
- method: 'resolveUserIdentifier',
1068
- userId: identifier.userId,
1069
- userName: identifier.userName,
1070
- workspaceId,
1071
- strictWorkspace
1072
- });
1073
- // Build ordered workspace IDs: specified workspace first (or only when strict).
1074
- const allWorkspaces = await this.getWorkspaces();
1075
- let orderedWorkspaceIds;
1076
- if (workspaceId) {
1077
- if (strictWorkspace) {
1078
- orderedWorkspaceIds = [workspaceId];
1079
- }
1080
- else {
1081
- const allWorkspaceIds = allWorkspaces.map(workspace => workspace.id);
1082
- if (allWorkspaceIds.includes(workspaceId)) {
1083
- const otherWorkspaceIds = allWorkspaceIds.filter(id => id !== workspaceId);
1084
- orderedWorkspaceIds = [workspaceId, ...otherWorkspaceIds];
1085
- }
1086
- else {
1087
- // Keep prior behavior when workspaceId is unknown: search known workspaces.
1088
- orderedWorkspaceIds = allWorkspaceIds;
1089
- }
1090
- }
1091
- }
1092
- else {
1093
- orderedWorkspaceIds = allWorkspaces.map(workspace => workspace.id);
1094
- }
1095
- // If userId is provided, search by ID
1096
- if (identifier.userId) {
1097
- for (const searchWorkspaceId of orderedWorkspaceIds) {
1098
- try {
1099
- const users = await this.getUsers(searchWorkspaceId);
1100
- const user = users.find(u => u.id === identifier.userId);
1101
- if (user) {
1102
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'User resolved by ID', {
1103
- method: 'resolveUserIdentifier',
1104
- userId: identifier.userId,
1105
- userName: user.name,
1106
- foundInWorkspaceId: searchWorkspaceId
1107
- });
1108
- return user;
1109
- }
1110
- }
1111
- catch (workspaceError) {
1112
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for user by ID', {
1113
- method: 'resolveUserIdentifier',
1114
- userId: identifier.userId,
1115
- workspaceId: searchWorkspaceId,
1116
- error: getErrorMessage(workspaceError)
1117
- });
1118
- }
1119
- }
1120
- }
1121
- // If userName is provided, search by name/email
1122
- if (identifier.userName) {
1123
- const searchTerm = identifier.userName.toLowerCase();
1124
- for (const searchWorkspaceId of orderedWorkspaceIds) {
1125
- try {
1126
- const users = await this.getUsers(searchWorkspaceId);
1127
- const user = users.find(u => u.name?.toLowerCase().includes(searchTerm) ||
1128
- u.email?.toLowerCase().includes(searchTerm));
1129
- if (user) {
1130
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'User resolved by name/email', {
1131
- method: 'resolveUserIdentifier',
1132
- userName: identifier.userName,
1133
- userId: user.id,
1134
- foundInWorkspaceId: searchWorkspaceId
1135
- });
1136
- return user;
1137
- }
1138
- }
1139
- catch (workspaceError) {
1140
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for user by name', {
1141
- method: 'resolveUserIdentifier',
1142
- userName: identifier.userName,
1143
- workspaceId: searchWorkspaceId,
1144
- error: getErrorMessage(workspaceError)
1145
- });
1146
- }
1147
- }
1148
- }
1149
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve user identifier', {
1150
- method: 'resolveUserIdentifier',
1151
- userId: identifier.userId,
1152
- userName: identifier.userName,
1153
- workspaceId
1154
- });
1155
- return undefined;
1156
- }
1157
- catch (error) {
1158
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Error resolving user identifier', {
1159
- method: 'resolveUserIdentifier',
1160
- userId: identifier.userId,
1161
- userName: identifier.userName,
1162
- error: getErrorMessage(error)
1163
- });
1164
- throw error;
1165
- }
87
+ async createProject(projectData) {
88
+ return (0, projects_1.createProject)(this._ctx, projectData);
1166
89
  }
1167
- async getProjectByName(projectName, workspaceId) {
1168
- try {
1169
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Finding project by name', {
1170
- method: 'getProjectByName',
1171
- projectName,
1172
- workspaceId
1173
- });
1174
- // First, search in the specified workspace (if provided)
1175
- if (workspaceId) {
1176
- const { items: projects } = await this.getProjects(workspaceId);
1177
- const project = projects.find(p => p.name === projectName);
1178
- if (project) {
1179
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project found by name in specified workspace', {
1180
- method: 'getProjectByName',
1181
- projectName,
1182
- projectId: project.id,
1183
- workspaceId
1184
- });
1185
- return project;
1186
- }
1187
- }
1188
- // If not found in specified workspace (or none specified), search across all other workspaces
1189
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Project not found in specified workspace, searching all workspaces', {
1190
- method: 'getProjectByName',
1191
- projectName,
1192
- specifiedWorkspaceId: workspaceId
1193
- });
1194
- const allWorkspaces = await this.getWorkspaces();
1195
- const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
1196
- for (const workspace of otherWorkspaces) {
1197
- try {
1198
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching workspace for project', {
1199
- method: 'getProjectByName',
1200
- projectName,
1201
- searchingWorkspaceId: workspace.id,
1202
- searchingWorkspaceName: workspace.name
1203
- });
1204
- const { items: workspaceProjects } = await this.getProjects(workspace.id);
1205
- const foundProject = workspaceProjects.find(p => p.name === projectName);
1206
- if (foundProject) {
1207
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Project found by name in different workspace', {
1208
- method: 'getProjectByName',
1209
- projectName,
1210
- projectId: foundProject.id,
1211
- foundInWorkspaceId: workspace.id,
1212
- foundInWorkspaceName: workspace.name,
1213
- originalWorkspaceId: workspaceId
1214
- });
1215
- return foundProject;
1216
- }
1217
- }
1218
- catch (workspaceError) {
1219
- // Log error but continue searching other workspaces
1220
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for project', {
1221
- method: 'getProjectByName',
1222
- projectName,
1223
- workspaceId: workspace.id,
1224
- workspaceName: workspace.name,
1225
- error: getErrorMessage(workspaceError)
1226
- });
1227
- }
1228
- }
1229
- // Project not found in any workspace
1230
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Project not found by name in any workspace', {
1231
- method: 'getProjectByName',
1232
- projectName,
1233
- searchedWorkspaces: [workspaceId, ...otherWorkspaces.map(w => w.id)],
1234
- totalWorkspacesSearched: allWorkspaces.length
1235
- });
1236
- return undefined;
1237
- }
1238
- catch (error) {
1239
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to find project by name', {
1240
- method: 'getProjectByName',
1241
- projectName,
1242
- error: getErrorMessage(error)
1243
- });
1244
- throw error;
1245
- }
90
+ async updateProject(projectId, updates) {
91
+ return (0, projects_1.updateProject)(this._ctx, projectId, updates);
1246
92
  }
1247
- async searchTasks(query, workspaceId, limit) {
1248
- try {
1249
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching tasks', {
1250
- method: 'searchTasks',
1251
- query,
1252
- workspaceId,
1253
- limit
1254
- });
1255
- // Apply search limit to prevent resource exhaustion
1256
- const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1257
- const lowerQuery = query.toLowerCase();
1258
- const allMatchingTasks = [];
1259
- let aggregateTruncation;
1260
- // First, search in the specified workspace
1261
- const { items: primaryTasks, truncation: primaryTruncation } = await this.getTasks({
1262
- workspaceId,
1263
- limit: (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingTasks.length, effectiveLimit),
1264
- maxPages: constants_1.LIMITS.MAX_PAGES
1265
- });
1266
- aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, primaryTruncation);
1267
- const primaryMatches = primaryTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
1268
- task.description?.toLowerCase().includes(lowerQuery));
1269
- allMatchingTasks.push(...primaryMatches.slice(0, effectiveLimit));
1270
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
1271
- method: 'searchTasks',
1272
- query,
1273
- primaryWorkspaceId: workspaceId,
1274
- primaryMatches: primaryMatches.length,
1275
- keptMatches: allMatchingTasks.length
1276
- });
1277
- // If we haven't reached the limit, search other workspaces
1278
- if (allMatchingTasks.length < effectiveLimit) {
1279
- try {
1280
- const allWorkspaces = await this.getWorkspaces();
1281
- const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
1282
- for (const workspace of otherWorkspaces) {
1283
- if (allMatchingTasks.length >= effectiveLimit)
1284
- break;
1285
- try {
1286
- // Calculate fetch limit before API call (defense-in-depth)
1287
- const fetchLimit = (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingTasks.length, effectiveLimit);
1288
- if (fetchLimit <= 0)
1289
- break;
1290
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for tasks', {
1291
- method: 'searchTasks',
1292
- query,
1293
- searchingWorkspaceId: workspace.id,
1294
- searchingWorkspaceName: workspace.name,
1295
- remainingNeeded: effectiveLimit - allMatchingTasks.length
1296
- });
1297
- const { items: workspaceTasks, truncation: wsTruncation } = await this.getTasks({
1298
- workspaceId: workspace.id,
1299
- limit: fetchLimit,
1300
- maxPages: constants_1.LIMITS.MAX_PAGES
1301
- });
1302
- aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, wsTruncation);
1303
- const workspaceMatches = workspaceTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
1304
- task.description?.toLowerCase().includes(lowerQuery));
1305
- // Only add as many as we still need
1306
- const remaining = effectiveLimit - allMatchingTasks.length;
1307
- allMatchingTasks.push(...workspaceMatches.slice(0, remaining));
1308
- if (workspaceMatches.length > 0) {
1309
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
1310
- method: 'searchTasks',
1311
- query,
1312
- workspaceId: workspace.id,
1313
- workspaceName: workspace.name,
1314
- matches: workspaceMatches.length,
1315
- keptMatches: Math.min(workspaceMatches.length, remaining)
1316
- });
1317
- }
1318
- }
1319
- catch (workspaceError) {
1320
- // Log error but continue searching other workspaces
1321
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for tasks', {
1322
- method: 'searchTasks',
1323
- query,
1324
- workspaceId: workspace.id,
1325
- workspaceName: workspace.name,
1326
- error: getErrorMessage(workspaceError)
1327
- });
1328
- }
1329
- }
1330
- }
1331
- catch (workspaceListError) {
1332
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to get workspace list for cross-workspace search', {
1333
- method: 'searchTasks',
1334
- query,
1335
- error: getErrorMessage(workspaceListError)
1336
- });
1337
- }
1338
- }
1339
- // Results are already limited during collection, no need to slice again
1340
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task search completed across all workspaces', {
1341
- method: 'searchTasks',
1342
- query,
1343
- returnedResults: allMatchingTasks.length,
1344
- limit: effectiveLimit
1345
- });
1346
- if (aggregateTruncation) {
1347
- aggregateTruncation.returnedCount = allMatchingTasks.length;
1348
- }
1349
- return { items: allMatchingTasks, truncation: aggregateTruncation };
1350
- }
1351
- catch (error) {
1352
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search tasks', {
1353
- method: 'searchTasks',
1354
- query,
1355
- error: getErrorMessage(error)
1356
- });
1357
- throw error;
1358
- }
93
+ async deleteProject(projectId) {
94
+ return (0, projects_1.deleteProject)(this._ctx, projectId);
1359
95
  }
1360
- async searchProjects(query, workspaceId, limit) {
1361
- try {
1362
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching projects', {
1363
- method: 'searchProjects',
1364
- query,
1365
- workspaceId,
1366
- limit
1367
- });
1368
- // Apply search limit to prevent resource exhaustion
1369
- const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
1370
- const lowerQuery = query.toLowerCase();
1371
- const allMatchingProjects = [];
1372
- let aggregateTruncation;
1373
- // First, search in the specified workspace
1374
- const { items: primaryProjects, truncation: primaryTruncation } = await this.getProjects(workspaceId, {
1375
- maxPages: constants_1.LIMITS.MAX_PAGES,
1376
- limit: (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingProjects.length, effectiveLimit)
1377
- });
1378
- aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, primaryTruncation);
1379
- const primaryMatches = primaryProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
1380
- project.description?.toLowerCase().includes(lowerQuery));
1381
- allMatchingProjects.push(...primaryMatches.slice(0, effectiveLimit));
1382
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
1383
- method: 'searchProjects',
1384
- query,
1385
- primaryWorkspaceId: workspaceId,
1386
- primaryMatches: primaryMatches.length,
1387
- keptMatches: allMatchingProjects.length
1388
- });
1389
- // If we haven't reached the limit, search other workspaces
1390
- if (allMatchingProjects.length < effectiveLimit) {
1391
- try {
1392
- const allWorkspaces = await this.getWorkspaces();
1393
- const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
1394
- for (const workspace of otherWorkspaces) {
1395
- if (allMatchingProjects.length >= effectiveLimit)
1396
- break;
1397
- try {
1398
- // Calculate fetch limit before API call (defense-in-depth)
1399
- const fetchLimit = (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingProjects.length, effectiveLimit);
1400
- if (fetchLimit <= 0)
1401
- break;
1402
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for projects', {
1403
- method: 'searchProjects',
1404
- query,
1405
- searchingWorkspaceId: workspace.id,
1406
- searchingWorkspaceName: workspace.name,
1407
- remainingNeeded: effectiveLimit - allMatchingProjects.length
1408
- });
1409
- const { items: workspaceProjects, truncation: wsTruncation } = await this.getProjects(workspace.id, {
1410
- maxPages: constants_1.LIMITS.MAX_PAGES,
1411
- limit: fetchLimit
1412
- });
1413
- aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, wsTruncation);
1414
- const workspaceMatches = workspaceProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
1415
- project.description?.toLowerCase().includes(lowerQuery));
1416
- // Only add as many as we still need
1417
- const remaining = effectiveLimit - allMatchingProjects.length;
1418
- allMatchingProjects.push(...workspaceMatches.slice(0, remaining));
1419
- if (workspaceMatches.length > 0) {
1420
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
1421
- method: 'searchProjects',
1422
- query,
1423
- workspaceId: workspace.id,
1424
- workspaceName: workspace.name,
1425
- matches: workspaceMatches.length,
1426
- keptMatches: Math.min(workspaceMatches.length, remaining)
1427
- });
1428
- }
1429
- }
1430
- catch (workspaceError) {
1431
- // Log error but continue searching other workspaces
1432
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for projects', {
1433
- method: 'searchProjects',
1434
- query,
1435
- workspaceId: workspace.id,
1436
- workspaceName: workspace.name,
1437
- error: getErrorMessage(workspaceError)
1438
- });
1439
- }
1440
- }
1441
- }
1442
- catch (workspaceListError) {
1443
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to get workspace list for cross-workspace search', {
1444
- method: 'searchProjects',
1445
- query,
1446
- error: getErrorMessage(workspaceListError)
1447
- });
1448
- }
1449
- }
1450
- // Results are already limited during collection, no need to slice again
1451
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project search completed across all workspaces', {
1452
- method: 'searchProjects',
1453
- query,
1454
- returnedResults: allMatchingProjects.length,
1455
- limit: effectiveLimit
1456
- });
1457
- if (aggregateTruncation) {
1458
- aggregateTruncation.returnedCount = allMatchingProjects.length;
1459
- }
1460
- return { items: allMatchingProjects, truncation: aggregateTruncation };
1461
- }
1462
- catch (error) {
1463
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search projects', {
1464
- method: 'searchProjects',
1465
- query,
1466
- error: getErrorMessage(error)
1467
- });
1468
- throw error;
1469
- }
96
+ async getProjectByName(projectName, workspaceId) {
97
+ return (0, projects_1.getProjectByName)(this._ctx, projectName, workspaceId);
1470
98
  }
1471
99
  // ========================================
1472
100
  // COMMENT API METHODS
1473
101
  // ========================================
1474
- /**
1475
- * Get comments for a task with proper pagination support
1476
- * @param taskId Task ID to get comments for
1477
- * @param cursor Optional cursor for pagination
1478
- * @returns Paginated response with comments and metadata
1479
- */
1480
102
  async getComments(taskId, cursor) {
1481
- const cacheParams = { taskId, cursor: cursor || null };
1482
- const cacheKey = `comments:${JSON.stringify(cacheParams)}`;
1483
- return this.commentCache.withCache(cacheKey, async () => {
1484
- try {
1485
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching comments from Motion API', {
1486
- method: 'getComments',
1487
- taskId,
1488
- cursor
1489
- });
1490
- const params = new URLSearchParams({ taskId });
1491
- if (cursor)
1492
- params.append('cursor', cursor);
1493
- const response = await this.requestWithRetry(() => this.client.get(`/comments?${params.toString()}`));
1494
- // Use new response wrapper for consistent handling
1495
- const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'comments');
1496
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Comments fetched successfully', {
1497
- method: 'getComments',
1498
- count: unwrapped.data.length,
1499
- hasMore: !!unwrapped.meta?.nextCursor,
1500
- taskId
1501
- });
1502
- // Return in our standard paginated format
1503
- return {
1504
- data: unwrapped.data,
1505
- meta: {
1506
- nextCursor: unwrapped.meta?.nextCursor,
1507
- pageSize: unwrapped.meta?.pageSize || unwrapped.data.length
1508
- }
1509
- };
1510
- }
1511
- catch (error) {
1512
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch comments', {
1513
- method: 'getComments',
1514
- error: getErrorMessage(error),
1515
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1516
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1517
- taskId,
1518
- cursor
1519
- });
1520
- throw this.formatApiError(error, 'fetch', 'comment');
1521
- }
1522
- });
103
+ return (0, comments_1.getComments)(this._ctx, taskId, cursor);
1523
104
  }
1524
105
  async createComment(commentData) {
1525
- try {
1526
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating comment in Motion API', {
1527
- method: 'createComment',
1528
- taskId: commentData.taskId,
1529
- contentLength: commentData.content?.length || 0
1530
- });
1531
- // Create minimal payload by removing empty/null values to avoid validation errors
1532
- const minimalPayload = (0, constants_1.createMinimalPayload)(commentData);
1533
- const response = await this.requestWithRetry(() => this.client.post('/comments', minimalPayload));
1534
- // Invalidate all cached pages for this task's comments (cursor variants included)
1535
- const cachePrefix = `comments:{"taskId":${JSON.stringify(commentData.taskId)}`;
1536
- this.commentCache.invalidate(cachePrefix);
1537
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Comment created successfully', {
1538
- method: 'createComment',
1539
- commentId: response.data?.id,
1540
- taskId: commentData.taskId
1541
- });
1542
- return response.data;
1543
- }
1544
- catch (error) {
1545
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create comment', {
1546
- method: 'createComment',
1547
- error: getErrorMessage(error),
1548
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1549
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1550
- taskId: commentData?.taskId
1551
- });
1552
- throw this.formatApiError(error, 'create', 'comment');
1553
- }
106
+ return (0, comments_1.createComment)(this._ctx, commentData);
1554
107
  }
1555
108
  // ========================================
1556
109
  // CUSTOM FIELD API METHODS
1557
110
  // ========================================
1558
- /**
1559
- * Fetch custom fields from Motion API
1560
- * @param workspaceId - Required workspace ID to get custom fields for
1561
- * @returns Array of custom fields
1562
- */
1563
111
  async getCustomFields(workspaceId) {
1564
- const cacheKey = `custom-fields:${workspaceId}`;
1565
- return this.customFieldCache.withCache(cacheKey, async () => {
1566
- try {
1567
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching custom fields from Motion API', {
1568
- method: 'getCustomFields',
1569
- workspaceId
1570
- });
1571
- const url = `/beta/workspaces/${workspaceId}/custom-fields`;
1572
- const response = await this.requestWithRetry(() => this.client.get(url));
1573
- // Beta API returns direct array, not wrapped
1574
- const fieldsArray = response.data || [];
1575
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom fields fetched successfully', {
1576
- method: 'getCustomFields',
1577
- count: fieldsArray.length,
1578
- workspaceId
1579
- });
1580
- return fieldsArray;
1581
- }
1582
- catch (error) {
1583
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch custom fields', {
1584
- method: 'getCustomFields',
1585
- error: getErrorMessage(error),
1586
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1587
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1588
- workspaceId
1589
- });
1590
- throw this.formatApiError(error, 'fetch', 'custom field');
1591
- }
1592
- });
112
+ return (0, customFields_1.getCustomFields)(this._ctx, workspaceId);
1593
113
  }
1594
- /**
1595
- * Create a new custom field
1596
- * @param workspaceId - Required workspace ID to create custom field in
1597
- * @param fieldData - Data for creating the custom field
1598
- * @returns The created custom field
1599
- */
1600
114
  async createCustomField(workspaceId, fieldData) {
1601
- try {
1602
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating custom field in Motion API', {
1603
- method: 'createCustomField',
1604
- name: fieldData.name,
1605
- field: fieldData.field,
1606
- workspaceId
1607
- });
1608
- // Transform payload to match Motion API expectations
1609
- // POST API expects 'type' in request, but returns 'field' in response
1610
- const apiPayload = {
1611
- name: fieldData.name,
1612
- type: fieldData.field, // Motion API POST expects 'type' property in request body
1613
- ...(fieldData.required !== undefined && { required: fieldData.required }),
1614
- ...(fieldData.metadata && { metadata: fieldData.metadata })
1615
- };
1616
- // Create minimal payload by removing empty/null values to avoid validation errors
1617
- const minimalPayload = (0, constants_1.createMinimalPayload)(apiPayload);
1618
- const response = await this.requestWithRetry(() => this.client.post(`/beta/workspaces/${workspaceId}/custom-fields`, minimalPayload));
1619
- // Invalidate cache after successful creation
1620
- this.customFieldCache.invalidate(`custom-fields:${workspaceId}`);
1621
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field created successfully', {
1622
- method: 'createCustomField',
1623
- fieldId: response.data?.id,
1624
- name: fieldData.name,
1625
- workspaceId
1626
- });
1627
- return response.data;
1628
- }
1629
- catch (error) {
1630
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create custom field', {
1631
- method: 'createCustomField',
1632
- error: getErrorMessage(error),
1633
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1634
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1635
- fieldName: fieldData?.name,
1636
- workspaceId
1637
- });
1638
- throw this.formatApiError(error, 'create', 'custom field', undefined, fieldData.name);
1639
- }
115
+ return (0, customFields_1.createCustomField)(this._ctx, workspaceId, fieldData);
1640
116
  }
1641
- /**
1642
- * Delete a custom field
1643
- * @param workspaceId - Required workspace ID containing the custom field
1644
- * @param fieldId - ID of the custom field to delete
1645
- * @returns Success indicator
1646
- */
1647
117
  async deleteCustomField(workspaceId, fieldId) {
1648
- try {
1649
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting custom field from Motion API', {
1650
- method: 'deleteCustomField',
1651
- fieldId,
1652
- workspaceId
1653
- });
1654
- await this.requestWithRetry(() => this.client.delete(`/beta/workspaces/${workspaceId}/custom-fields/${fieldId}`));
1655
- // Invalidate cache after successful deletion
1656
- this.customFieldCache.invalidate(`custom-fields:${workspaceId}`);
1657
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field deleted successfully', {
1658
- method: 'deleteCustomField',
1659
- fieldId,
1660
- workspaceId
1661
- });
1662
- return { success: true };
1663
- }
1664
- catch (error) {
1665
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete custom field', {
1666
- method: 'deleteCustomField',
1667
- error: getErrorMessage(error),
1668
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1669
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1670
- fieldId,
1671
- workspaceId
1672
- });
1673
- throw this.formatApiError(error, 'delete', 'custom field', fieldId);
1674
- }
118
+ return (0, customFields_1.deleteCustomField)(this._ctx, workspaceId, fieldId);
1675
119
  }
1676
- /**
1677
- * Add a custom field to a project
1678
- * @param projectId - ID of the project
1679
- * @param fieldId - ID of the custom field
1680
- * @param value - Optional value for the field
1681
- * @returns The field value as returned by the API ({ type, value })
1682
- */
1683
120
  async addCustomFieldToProject(projectId, fieldId, value, fieldType) {
1684
- try {
1685
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Adding custom field to project', {
1686
- method: 'addCustomFieldToProject',
1687
- projectId,
1688
- fieldId,
1689
- hasValue: value !== undefined
1690
- });
1691
- // API expects: { customFieldInstanceId, value: { type, value } }
1692
- // Value type uses camelCase per API docs (e.g., text, multiSelect)
1693
- const requestData = {
1694
- customFieldInstanceId: fieldId,
1695
- };
1696
- if (value !== undefined) {
1697
- if (value === null) {
1698
- requestData.value = fieldType !== undefined
1699
- ? { type: fieldType, value: null }
1700
- : { value: null };
1701
- }
1702
- else {
1703
- if (!fieldType) {
1704
- throw new Error('Field type is required when setting a non-null custom field value');
1705
- }
1706
- requestData.value = {
1707
- type: fieldType,
1708
- value
1709
- };
1710
- }
1711
- }
1712
- const response = await this.requestWithRetry(() => this.client.post(`/beta/custom-field-values/project/${projectId}`, requestData));
1713
- // Invalidate project cache broadly — the API response is { type, value },
1714
- // not a full project object, so we don't have workspace context
1715
- this.projectCache.invalidate(`projects:`);
1716
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to project successfully', {
1717
- method: 'addCustomFieldToProject',
1718
- projectId,
1719
- fieldId
1720
- });
1721
- return response.data;
1722
- }
1723
- catch (error) {
1724
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to add custom field to project', {
1725
- method: 'addCustomFieldToProject',
1726
- error: getErrorMessage(error),
1727
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1728
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1729
- projectId,
1730
- fieldId
1731
- });
1732
- throw this.formatApiError(error, 'update', 'project', projectId);
1733
- }
121
+ return (0, customFields_1.addCustomFieldToProject)(this._ctx, projectId, fieldId, value, fieldType);
1734
122
  }
1735
- /**
1736
- * Remove a custom field from a project
1737
- * @param projectId - ID of the project
1738
- * @param valueId - ID of the custom field value
1739
- * @returns Success indicator
1740
- */
1741
123
  async removeCustomFieldFromProject(projectId, valueId) {
1742
- try {
1743
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Removing custom field from project', {
1744
- method: 'removeCustomFieldFromProject',
1745
- projectId,
1746
- valueId
1747
- });
1748
- await this.requestWithRetry(() => this.client.delete(`/beta/custom-field-values/project/${projectId}/custom-fields/${valueId}`));
1749
- // Invalidate all project caches since we don't have workspace context here
1750
- this.projectCache.invalidate(`projects:`);
1751
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from project successfully', {
1752
- method: 'removeCustomFieldFromProject',
1753
- projectId,
1754
- valueId
1755
- });
1756
- return { success: true };
1757
- }
1758
- catch (error) {
1759
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to remove custom field from project', {
1760
- method: 'removeCustomFieldFromProject',
1761
- error: getErrorMessage(error),
1762
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1763
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1764
- projectId,
1765
- valueId
1766
- });
1767
- throw this.formatApiError(error, 'update', 'project', projectId);
1768
- }
124
+ return (0, customFields_1.removeCustomFieldFromProject)(this._ctx, projectId, valueId);
1769
125
  }
1770
- /**
1771
- * Add a custom field to a task
1772
- * @param taskId - ID of the task
1773
- * @param fieldId - ID of the custom field
1774
- * @param value - Optional value for the field
1775
- * @returns The field value as returned by the API ({ type, value })
1776
- */
1777
126
  async addCustomFieldToTask(taskId, fieldId, value, fieldType) {
1778
- try {
1779
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Adding custom field to task', {
1780
- method: 'addCustomFieldToTask',
1781
- taskId,
1782
- fieldId,
1783
- hasValue: value !== undefined
1784
- });
1785
- // API expects: { customFieldInstanceId, value: { type, value } }
1786
- // Value type uses camelCase per API docs (e.g., text, multiSelect)
1787
- const requestData = {
1788
- customFieldInstanceId: fieldId,
1789
- };
1790
- if (value !== undefined) {
1791
- if (value === null) {
1792
- requestData.value = fieldType !== undefined
1793
- ? { type: fieldType, value: null }
1794
- : { value: null };
1795
- }
1796
- else {
1797
- if (!fieldType) {
1798
- throw new Error('Field type is required when setting a non-null custom field value');
1799
- }
1800
- requestData.value = {
1801
- type: fieldType,
1802
- value
1803
- };
1804
- }
1805
- }
1806
- const response = await this.requestWithRetry(() => this.client.post(`/beta/custom-field-values/task/${taskId}`, requestData));
1807
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to task successfully', {
1808
- method: 'addCustomFieldToTask',
1809
- taskId,
1810
- fieldId
1811
- });
1812
- return response.data;
1813
- }
1814
- catch (error) {
1815
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to add custom field to task', {
1816
- method: 'addCustomFieldToTask',
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
- taskId,
1821
- fieldId
1822
- });
1823
- throw this.formatApiError(error, 'update', 'task', taskId);
1824
- }
127
+ return (0, customFields_1.addCustomFieldToTask)(this._ctx, taskId, fieldId, value, fieldType);
1825
128
  }
1826
- /**
1827
- * Remove a custom field from a task
1828
- * @param taskId - ID of the task
1829
- * @param valueId - ID of the custom field value
1830
- * @returns Success indicator
1831
- */
1832
129
  async removeCustomFieldFromTask(taskId, valueId) {
1833
- try {
1834
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Removing custom field from task', {
1835
- method: 'removeCustomFieldFromTask',
1836
- taskId,
1837
- valueId
1838
- });
1839
- await this.requestWithRetry(() => this.client.delete(`/beta/custom-field-values/task/${taskId}/custom-fields/${valueId}`));
1840
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from task successfully', {
1841
- method: 'removeCustomFieldFromTask',
1842
- taskId,
1843
- valueId
1844
- });
1845
- return { success: true };
1846
- }
1847
- catch (error) {
1848
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to remove custom field from task', {
1849
- method: 'removeCustomFieldFromTask',
1850
- error: getErrorMessage(error),
1851
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1852
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1853
- taskId,
1854
- valueId
1855
- });
1856
- throw this.formatApiError(error, 'update', 'task', taskId);
1857
- }
130
+ return (0, customFields_1.removeCustomFieldFromTask)(this._ctx, taskId, valueId);
1858
131
  }
1859
132
  // ========================================
1860
133
  // RECURRING TASK API METHODS
1861
134
  // ========================================
1862
- /**
1863
- * Fetch recurring tasks from Motion API with automatic pagination
1864
- * @param workspaceId - Optional workspace ID to filter recurring tasks
1865
- * @param options - Optional configuration for maxPages and limit
1866
- * @returns Array of recurring tasks from all pages
1867
- */
1868
135
  async getRecurringTasks(workspaceId, options) {
1869
- const { maxPages = 10, limit } = options || {};
1870
- const cacheKey = workspaceId ? `recurring-tasks:workspace:${workspaceId}` : 'recurring-tasks:all';
1871
- // Check cache - return items only (no stale truncation info)
1872
- const cachedItems = this.recurringTaskCache.get(cacheKey);
1873
- if (cachedItems !== null) {
1874
- return { items: cachedItems };
1875
- }
1876
- try {
1877
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching recurring tasks from Motion API with pagination', {
1878
- method: 'getRecurringTasks',
1879
- workspaceId,
1880
- maxPages,
1881
- limit
1882
- });
1883
- // Create a fetch function for pagination utility
1884
- const fetchPage = async (cursor) => {
1885
- const params = new URLSearchParams();
1886
- if (workspaceId)
1887
- params.append('workspaceId', workspaceId);
1888
- if (cursor)
1889
- params.append('cursor', cursor);
1890
- const queryString = params.toString();
1891
- const url = queryString ? `/recurring-tasks?${queryString}` : '/recurring-tasks';
1892
- return this.requestWithRetry(() => this.client.get(url));
1893
- };
1894
- // Use pagination utility to fetch all pages
1895
- const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'recurring-tasks', {
1896
- maxPages,
1897
- logProgress: true,
1898
- ...(limit ? { maxItems: limit } : {})
1899
- });
1900
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring tasks fetched successfully with pagination', {
1901
- method: 'getRecurringTasks',
1902
- totalCount: paginatedResult.totalFetched,
1903
- pagesProcessed: Math.ceil(paginatedResult.totalFetched / 50), // Assuming ~50 items per page
1904
- hasMore: paginatedResult.hasMore,
1905
- workspaceId
1906
- });
1907
- // Cache only items, not truncation metadata
1908
- this.recurringTaskCache.set(cacheKey, paginatedResult.items);
1909
- return { items: paginatedResult.items, truncation: paginatedResult.truncation };
1910
- }
1911
- catch (error) {
1912
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch recurring tasks', {
1913
- method: 'getRecurringTasks',
1914
- error: getErrorMessage(error),
1915
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1916
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1917
- workspaceId
1918
- });
1919
- throw this.formatApiError(error, 'fetch', 'recurring task');
1920
- }
136
+ return (0, recurringTasks_1.getRecurringTasks)(this._ctx, workspaceId, options);
1921
137
  }
1922
- /**
1923
- * Create a new recurring task
1924
- * @param taskData - Data for creating the recurring task
1925
- * @returns The created recurring task
1926
- */
1927
138
  async createRecurringTask(taskData) {
1928
- try {
1929
- // Validate frequency object before transformation
1930
- const freqValidation = (0, frequencyTransform_1.validateFrequencyObject)(taskData.frequency);
1931
- if (!freqValidation.valid) {
1932
- throw new Error(`Invalid frequency object: ${freqValidation.reason || 'Unknown reason'}`);
1933
- }
1934
- // Transform frequency object to API string format
1935
- const frequencyString = (0, frequencyTransform_1.transformFrequencyToApiString)(taskData.frequency);
1936
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating recurring task in Motion API', {
1937
- method: 'createRecurringTask',
1938
- name: taskData.name,
1939
- assigneeId: taskData.assigneeId,
1940
- frequency: frequencyString,
1941
- originalFrequency: taskData.frequency,
1942
- workspaceId: taskData.workspaceId
1943
- });
1944
- // Build API payload: extract endDate from frequency object to top-level,
1945
- // replace frequency object with transformed string
1946
- const { frequency: _freqObj, ...restTaskData } = taskData;
1947
- const apiPayload = {
1948
- ...restTaskData,
1949
- frequency: frequencyString,
1950
- ...(taskData.frequency.endDate && { endDate: taskData.frequency.endDate })
1951
- };
1952
- // Create minimal payload by removing empty/null values to avoid validation errors
1953
- const minimalPayload = (0, constants_1.createMinimalPayload)(apiPayload);
1954
- const response = await this.requestWithRetry(() => this.client.post('/recurring-tasks', minimalPayload));
1955
- // Invalidate cache after successful creation
1956
- this.recurringTaskCache.invalidate('recurring-tasks:');
1957
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring task created successfully', {
1958
- method: 'createRecurringTask',
1959
- taskId: response.data?.id,
1960
- name: taskData.name
1961
- });
1962
- return response.data;
1963
- }
1964
- catch (error) {
1965
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create recurring task', {
1966
- method: 'createRecurringTask',
1967
- error: getErrorMessage(error),
1968
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
1969
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
1970
- taskName: taskData?.name
1971
- });
1972
- throw this.formatApiError(error, 'create', 'recurring task', undefined, taskData.name);
1973
- }
139
+ return (0, recurringTasks_1.createRecurringTask)(this._ctx, taskData);
1974
140
  }
1975
- /**
1976
- * Delete a recurring task
1977
- * @param recurringTaskId - ID of the recurring task to delete
1978
- * @returns Success indicator
1979
- */
1980
141
  async deleteRecurringTask(recurringTaskId) {
1981
- try {
1982
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting recurring task from Motion API', {
1983
- method: 'deleteRecurringTask',
1984
- recurringTaskId
1985
- });
1986
- await this.requestWithRetry(() => this.client.delete(`/recurring-tasks/${recurringTaskId}`));
1987
- // Invalidate cache after successful deletion
1988
- this.recurringTaskCache.invalidate('recurring-tasks:');
1989
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring task deleted successfully', {
1990
- method: 'deleteRecurringTask',
1991
- recurringTaskId
1992
- });
1993
- return { success: true };
1994
- }
1995
- catch (error) {
1996
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete recurring task', {
1997
- method: 'deleteRecurringTask',
1998
- error: getErrorMessage(error),
1999
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
2000
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
2001
- recurringTaskId
2002
- });
2003
- throw this.formatApiError(error, 'delete', 'recurring task', recurringTaskId);
2004
- }
142
+ return (0, recurringTasks_1.deleteRecurringTask)(this._ctx, recurringTaskId);
2005
143
  }
2006
144
  // ========================================
2007
145
  // SCHEDULE API METHODS
2008
146
  // ========================================
2009
- /**
2010
- * Get available schedule names for auto-scheduling
2011
- * @param workspaceId - Optional workspace ID to filter schedules (currently unused by Motion API)
2012
- * @returns Array of schedule names
2013
- */
2014
147
  async getAvailableScheduleNames(workspaceId) {
2015
- try {
2016
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching available schedule names', {
2017
- method: 'getAvailableScheduleNames',
2018
- workspaceId
2019
- });
2020
- // Fetch all schedules without filters to get available schedule templates
2021
- const schedules = await this.getSchedules();
2022
- const scheduleNames = schedules.map(schedule => schedule.name).filter(Boolean);
2023
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Available schedule names fetched successfully', {
2024
- method: 'getAvailableScheduleNames',
2025
- count: scheduleNames.length,
2026
- scheduleNames
2027
- });
2028
- return scheduleNames;
2029
- }
2030
- catch (error) {
2031
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch available schedule names', {
2032
- method: 'getAvailableScheduleNames',
2033
- error: getErrorMessage(error),
2034
- workspaceId
2035
- });
2036
- throw this.formatApiError(error, 'fetch', 'schedule');
2037
- }
148
+ return (0, schedules_1.getAvailableScheduleNames)(this._ctx, workspaceId);
2038
149
  }
2039
- /**
2040
- * Fetch schedules from Motion API.
2041
- * The Motion API GET /schedules accepts no query parameters.
2042
- * @returns Array of schedules
2043
- */
2044
150
  async getSchedules() {
2045
- const cacheKey = 'schedules:all';
2046
- return this.scheduleCache.withCache(cacheKey, async () => {
2047
- try {
2048
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching schedules from Motion API', {
2049
- method: 'getSchedules'
2050
- });
2051
- const response = await this.requestWithRetry(() => this.client.get('/schedules'));
2052
- // Validate response against schema
2053
- const validatedResponse = this.validateResponse(response.data, motion_1.SchedulesListResponseSchema, 'getSchedules');
2054
- // Handle both wrapped and unwrapped responses
2055
- const schedules = Array.isArray(validatedResponse)
2056
- ? validatedResponse
2057
- : validatedResponse.schedules || [];
2058
- const schedulesArray = Array.isArray(schedules) ? schedules : [];
2059
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Schedules fetched successfully', {
2060
- method: 'getSchedules',
2061
- count: schedulesArray.length
2062
- });
2063
- return schedulesArray;
2064
- }
2065
- catch (error) {
2066
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch schedules', {
2067
- method: 'getSchedules',
2068
- error: getErrorMessage(error),
2069
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
2070
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
2071
- });
2072
- throw this.formatApiError(error, 'fetch', 'schedule');
2073
- }
2074
- });
151
+ return (0, schedules_1.getSchedules)(this._ctx);
2075
152
  }
2076
153
  // ========================================
2077
154
  // STATUS API METHODS
2078
155
  // ========================================
2079
- /**
2080
- * Retrieves available workflow statuses from Motion
2081
- * @param workspaceId - Optional workspace ID to filter statuses
2082
- * @returns Promise resolving to array of Motion statuses
2083
- * @throws {Error} If the API request fails
2084
- */
2085
156
  async getStatuses(workspaceId) {
2086
- // Use workspace ID for cache key, or 'all' if not specified
2087
- const cacheKey = workspaceId ? `statuses:workspace:${workspaceId}` : 'statuses:all';
2088
- return this.statusCache.withCache(cacheKey, async () => {
2089
- try {
2090
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching statuses from Motion API', {
2091
- method: 'getStatuses',
2092
- workspaceId
2093
- });
2094
- const params = new URLSearchParams();
2095
- if (workspaceId)
2096
- params.append('workspaceId', workspaceId);
2097
- const queryString = params.toString();
2098
- const url = queryString ? `/statuses?${queryString}` : '/statuses';
2099
- const response = await this.requestWithRetry(() => this.client.get(url));
2100
- // Validate response against schema
2101
- const validatedResponse = this.validateResponse(response.data, motion_1.StatusesListResponseSchema, 'statuses');
2102
- // Extract statuses from validated response; fail loudly on unknown shape to avoid caching empty results.
2103
- let statuses;
2104
- if (Array.isArray(validatedResponse)) {
2105
- statuses = validatedResponse;
2106
- }
2107
- else if (validatedResponse &&
2108
- typeof validatedResponse === 'object' &&
2109
- 'statuses' in validatedResponse &&
2110
- Array.isArray(validatedResponse.statuses)) {
2111
- statuses = validatedResponse.statuses;
2112
- }
2113
- else {
2114
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Unexpected statuses response shape', {
2115
- method: 'getStatuses',
2116
- workspaceId,
2117
- responseType: validatedResponse === null ? 'null' : typeof validatedResponse
2118
- });
2119
- throw new Error('Invalid statuses response shape from Motion API');
2120
- }
2121
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Statuses fetched successfully', {
2122
- method: 'getStatuses',
2123
- count: statuses.length,
2124
- workspaceId
2125
- });
2126
- return statuses;
2127
- }
2128
- catch (error) {
2129
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch statuses', {
2130
- method: 'getStatuses',
2131
- error: getErrorMessage(error),
2132
- apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
2133
- apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
2134
- workspaceId
2135
- });
2136
- throw this.formatApiError(error, 'fetch', 'status');
2137
- }
2138
- });
157
+ return (0, statuses_1.getStatuses)(this._ctx, workspaceId);
2139
158
  }
2140
159
  // ========================================
2141
- // UTILITY METHODS
160
+ // SEARCH AND RESOLUTION METHODS
2142
161
  // ========================================
2143
- /**
2144
- * Get all uncompleted tasks across all workspaces and projects
2145
- * Filters tasks where status.isResolvedStatus is false or undefined
2146
- */
2147
- async getAllUncompletedTasks(limit, assigneeId) {
2148
- try {
2149
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching all uncompleted tasks across workspaces', {
2150
- method: 'getAllUncompletedTasks',
2151
- limit,
2152
- assigneeId
2153
- });
2154
- // Apply limit to prevent resource exhaustion
2155
- const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
2156
- const allUncompletedTasks = [];
2157
- let aggregateTruncation;
2158
- try {
2159
- // Get all workspaces
2160
- const workspaces = await this.getWorkspaces();
2161
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching for uncompleted tasks across workspaces', {
2162
- method: 'getAllUncompletedTasks',
2163
- totalWorkspaces: workspaces.length
2164
- });
2165
- // Fetch tasks from each workspace
2166
- for (const workspace of workspaces) {
2167
- if (allUncompletedTasks.length >= effectiveLimit) {
2168
- break; // Stop if we've reached the limit
2169
- }
2170
- try {
2171
- // Calculate fetch limit before API call (defense-in-depth)
2172
- const fetchLimit = (0, paginationNew_1.calculateAdaptiveFetchLimit)(allUncompletedTasks.length, effectiveLimit);
2173
- if (fetchLimit <= 0)
2174
- break;
2175
- // Get tasks from this workspace with adaptive limit
2176
- const { items: workspaceTasks, truncation: wsTruncation } = await this.getTasks({
2177
- workspaceId: workspace.id,
2178
- assigneeId,
2179
- limit: fetchLimit,
2180
- maxPages: constants_1.LIMITS.MAX_PAGES
2181
- });
2182
- aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, wsTruncation);
2183
- // Filter for uncompleted tasks
2184
- const uncompletedTasks = workspaceTasks.filter(task => {
2185
- // Task is uncompleted if status is missing or isResolvedStatus is false
2186
- if (!task.status)
2187
- return true; // No status = not resolved
2188
- if (typeof task.status === 'string') {
2189
- const resolvedStatusNames = new Set([
2190
- 'completed',
2191
- 'complete',
2192
- 'done',
2193
- 'closed',
2194
- 'resolved',
2195
- 'canceled',
2196
- 'cancelled'
2197
- ]);
2198
- return !resolvedStatusNames.has(task.status.trim().toLowerCase());
2199
- }
2200
- return !task.status.isResolvedStatus; // Object status with isResolvedStatus false
2201
- });
2202
- // Only add as many as we still need
2203
- const remaining = effectiveLimit - allUncompletedTasks.length;
2204
- allUncompletedTasks.push(...uncompletedTasks.slice(0, remaining));
2205
- if (uncompletedTasks.length > 0) {
2206
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found uncompleted tasks in workspace', {
2207
- method: 'getAllUncompletedTasks',
2208
- workspaceId: workspace.id,
2209
- workspaceName: workspace.name,
2210
- uncompletedTasks: uncompletedTasks.length,
2211
- keptTasks: Math.min(uncompletedTasks.length, remaining),
2212
- totalTasks: workspaceTasks.length
2213
- });
2214
- }
2215
- }
2216
- catch (workspaceError) {
2217
- // Log error but continue with other workspaces
2218
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to fetch tasks from workspace', {
2219
- method: 'getAllUncompletedTasks',
2220
- workspaceId: workspace.id,
2221
- workspaceName: workspace.name,
2222
- error: getErrorMessage(workspaceError)
2223
- });
2224
- }
2225
- }
2226
- }
2227
- catch (workspaceListError) {
2228
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to get workspace list', {
2229
- method: 'getAllUncompletedTasks',
2230
- error: getErrorMessage(workspaceListError)
2231
- });
2232
- throw workspaceListError;
2233
- }
2234
- // Results are already limited during collection, no need to slice again
2235
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'All uncompleted tasks fetched successfully', {
2236
- method: 'getAllUncompletedTasks',
2237
- returned: allUncompletedTasks.length,
2238
- limit: effectiveLimit
2239
- });
2240
- if (aggregateTruncation) {
2241
- aggregateTruncation.returnedCount = allUncompletedTasks.length;
2242
- }
2243
- return { items: allUncompletedTasks, truncation: aggregateTruncation };
2244
- }
2245
- catch (error) {
2246
- (0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch all uncompleted tasks', {
2247
- method: 'getAllUncompletedTasks',
2248
- error: getErrorMessage(error)
2249
- });
2250
- throw error;
2251
- }
162
+ async searchTasks(query, workspaceId, limit) {
163
+ return (0, search_1.searchTasks)(this._ctx, query, workspaceId, limit);
164
+ }
165
+ async searchProjects(query, workspaceId, limit) {
166
+ return (0, search_1.searchProjects)(this._ctx, query, workspaceId, limit);
167
+ }
168
+ async resolveProjectIdentifier(identifier, workspaceId) {
169
+ return (0, search_1.resolveProjectIdentifier)(this._ctx, identifier, workspaceId);
170
+ }
171
+ async resolveUserIdentifier(identifier, workspaceId, options) {
172
+ return (0, search_1.resolveUserIdentifier)(this._ctx, identifier, workspaceId, options);
2252
173
  }
2253
174
  }
2254
175
  exports.MotionApiService = MotionApiService;