motionmcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +15 -0
- package/.env.example +3 -0
- package/README.md +510 -0
- package/package.json +26 -0
- package/sample.png +0 -0
- package/src/index.js +47 -0
- package/src/mcp-server.js +1137 -0
- package/src/routes/motion.js +152 -0
- package/src/services/motionApi.js +1177 -0
- package/src/worker.js +248 -0
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
// MCP-compliant logger: outputs structured JSON to stderr
|
|
4
|
+
const mcpLog = (level, message, extra = {}) => {
|
|
5
|
+
const logEntry = {
|
|
6
|
+
level,
|
|
7
|
+
msg: message,
|
|
8
|
+
time: new Date().toISOString(),
|
|
9
|
+
...extra
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// MCP servers should log to stderr in JSON format
|
|
13
|
+
console.error(JSON.stringify(logEntry));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class MotionApiService {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.apiKey = process.env.MOTION_API_KEY;
|
|
19
|
+
this.baseUrl = 'https://api.usemotion.com/v1';
|
|
20
|
+
|
|
21
|
+
if (!this.apiKey) {
|
|
22
|
+
mcpLog('error', 'Motion API key not found in environment variables', {
|
|
23
|
+
component: 'MotionApiService',
|
|
24
|
+
method: 'constructor'
|
|
25
|
+
});
|
|
26
|
+
throw new Error('MOTION_API_KEY environment variable is required');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
mcpLog('info', 'Initializing Motion API service', {
|
|
30
|
+
component: 'MotionApiService',
|
|
31
|
+
baseUrl: this.baseUrl
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.client = axios.create({
|
|
35
|
+
baseURL: this.baseUrl,
|
|
36
|
+
headers: {
|
|
37
|
+
'X-API-Key': `${this.apiKey}`,
|
|
38
|
+
'Content-Type': 'application/json'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.client.interceptors.response.use(
|
|
43
|
+
response => {
|
|
44
|
+
mcpLog('info', 'Motion API response successful', {
|
|
45
|
+
url: response.config?.url,
|
|
46
|
+
method: response.config?.method?.toUpperCase(),
|
|
47
|
+
status: response.status,
|
|
48
|
+
component: 'MotionApiService'
|
|
49
|
+
});
|
|
50
|
+
return response;
|
|
51
|
+
},
|
|
52
|
+
error => {
|
|
53
|
+
const errorDetails = {
|
|
54
|
+
url: error.config?.url,
|
|
55
|
+
method: error.config?.method?.toUpperCase(),
|
|
56
|
+
status: error.response?.status,
|
|
57
|
+
statusText: error.response?.statusText,
|
|
58
|
+
apiMessage: error.response?.data?.message,
|
|
59
|
+
errorMessage: error.message,
|
|
60
|
+
component: 'MotionApiService'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
mcpLog('error', 'Motion API request failed', errorDetails);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getProjects(workspaceId = null) {
|
|
70
|
+
try {
|
|
71
|
+
mcpLog('debug', 'Fetching projects from Motion API', {
|
|
72
|
+
method: 'getProjects',
|
|
73
|
+
workspaceId
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// If no workspace ID provided, try to get the first available workspace
|
|
77
|
+
if (!workspaceId) {
|
|
78
|
+
try {
|
|
79
|
+
const workspaces = await this.getWorkspaces();
|
|
80
|
+
if (workspaces && workspaces.length > 0) {
|
|
81
|
+
workspaceId = workspaces[0].id;
|
|
82
|
+
mcpLog('info', 'Using first available workspace for projects', {
|
|
83
|
+
method: 'getProjects',
|
|
84
|
+
workspaceId,
|
|
85
|
+
workspaceName: workspaces[0].name
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} catch (workspaceError) {
|
|
89
|
+
mcpLog('warn', 'Could not fetch workspace for projects', {
|
|
90
|
+
method: 'getProjects',
|
|
91
|
+
error: workspaceError.message
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Build the query string with workspace ID if available
|
|
97
|
+
const params = new URLSearchParams();
|
|
98
|
+
if (workspaceId) {
|
|
99
|
+
params.append('workspaceId', workspaceId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const url = `/projects${params.toString() ? '?' + params.toString() : ''}`;
|
|
103
|
+
const response = await this.client.get(url);
|
|
104
|
+
|
|
105
|
+
// Handle Motion API response structure - projects are wrapped in a projects array
|
|
106
|
+
const projects = response.data?.projects || response.data || [];
|
|
107
|
+
|
|
108
|
+
mcpLog('info', 'Successfully fetched projects', {
|
|
109
|
+
method: 'getProjects',
|
|
110
|
+
count: projects.length,
|
|
111
|
+
workspaceId,
|
|
112
|
+
responseStructure: response.data?.projects ? 'wrapped' : 'direct'
|
|
113
|
+
});
|
|
114
|
+
return projects;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
mcpLog('error', 'Failed to fetch projects', {
|
|
117
|
+
method: 'getProjects',
|
|
118
|
+
error: error.message,
|
|
119
|
+
apiStatus: error.response?.status,
|
|
120
|
+
apiMessage: error.response?.data?.message,
|
|
121
|
+
workspaceId
|
|
122
|
+
});
|
|
123
|
+
throw new Error(`Failed to fetch projects: ${error.response?.data?.message || error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async createProject(projectData) {
|
|
128
|
+
try {
|
|
129
|
+
mcpLog('debug', 'Creating new project in Motion API', {
|
|
130
|
+
method: 'createProject',
|
|
131
|
+
projectName: projectData.name
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// If no workspace ID provided, try to get the default workspace
|
|
135
|
+
if (!projectData.workspaceId) {
|
|
136
|
+
try {
|
|
137
|
+
const defaultWorkspace = await this.getDefaultWorkspace();
|
|
138
|
+
projectData = { ...projectData, workspaceId: defaultWorkspace.id };
|
|
139
|
+
mcpLog('info', 'Using default workspace for new project', {
|
|
140
|
+
method: 'createProject',
|
|
141
|
+
workspaceId: defaultWorkspace.id,
|
|
142
|
+
workspaceName: defaultWorkspace.name
|
|
143
|
+
});
|
|
144
|
+
} catch (workspaceError) {
|
|
145
|
+
mcpLog('warn', 'Could not get default workspace for project creation', {
|
|
146
|
+
method: 'createProject',
|
|
147
|
+
error: workspaceError.message
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const response = await this.client.post('/projects', projectData);
|
|
153
|
+
mcpLog('info', 'Successfully created project', {
|
|
154
|
+
method: 'createProject',
|
|
155
|
+
projectId: response.data?.id,
|
|
156
|
+
projectName: response.data?.name,
|
|
157
|
+
workspaceId: projectData.workspaceId
|
|
158
|
+
});
|
|
159
|
+
return response.data;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
mcpLog('error', 'Failed to create project', {
|
|
162
|
+
method: 'createProject',
|
|
163
|
+
projectName: projectData.name,
|
|
164
|
+
error: error.message,
|
|
165
|
+
apiStatus: error.response?.status,
|
|
166
|
+
apiMessage: error.response?.data?.message
|
|
167
|
+
});
|
|
168
|
+
throw new Error(`Failed to create project: ${error.response?.data?.message || error.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async getProject(projectId) {
|
|
173
|
+
try {
|
|
174
|
+
mcpLog('debug', 'Fetching project details from Motion API', {
|
|
175
|
+
method: 'getProject',
|
|
176
|
+
projectId
|
|
177
|
+
});
|
|
178
|
+
const response = await this.client.get(`/projects/${projectId}`);
|
|
179
|
+
mcpLog('info', 'Successfully fetched project details', {
|
|
180
|
+
method: 'getProject',
|
|
181
|
+
projectId,
|
|
182
|
+
projectName: response.data?.name
|
|
183
|
+
});
|
|
184
|
+
return response.data;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
mcpLog('error', 'Failed to fetch project', {
|
|
187
|
+
method: 'getProject',
|
|
188
|
+
projectId,
|
|
189
|
+
error: error.message,
|
|
190
|
+
apiStatus: error.response?.status,
|
|
191
|
+
apiMessage: error.response?.data?.message
|
|
192
|
+
});
|
|
193
|
+
throw new Error(`Failed to fetch project: ${error.response?.data?.message || error.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async updateProject(projectId, projectData) {
|
|
198
|
+
try {
|
|
199
|
+
mcpLog('debug', 'Updating project in Motion API', {
|
|
200
|
+
method: 'updateProject',
|
|
201
|
+
projectId,
|
|
202
|
+
updateFields: Object.keys(projectData)
|
|
203
|
+
});
|
|
204
|
+
const response = await this.client.patch(`/projects/${projectId}`, projectData);
|
|
205
|
+
mcpLog('info', 'Successfully updated project', {
|
|
206
|
+
method: 'updateProject',
|
|
207
|
+
projectId,
|
|
208
|
+
projectName: response.data?.name
|
|
209
|
+
});
|
|
210
|
+
return response.data;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
mcpLog('error', 'Failed to update project', {
|
|
213
|
+
method: 'updateProject',
|
|
214
|
+
projectId,
|
|
215
|
+
error: error.message,
|
|
216
|
+
apiStatus: error.response?.status,
|
|
217
|
+
apiMessage: error.response?.data?.message
|
|
218
|
+
});
|
|
219
|
+
throw new Error(`Failed to update project: ${error.response?.data?.message || error.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async deleteProject(projectId) {
|
|
224
|
+
try {
|
|
225
|
+
mcpLog('debug', 'Deleting project from Motion API', {
|
|
226
|
+
method: 'deleteProject',
|
|
227
|
+
projectId
|
|
228
|
+
});
|
|
229
|
+
await this.client.delete(`/projects/${projectId}`);
|
|
230
|
+
mcpLog('info', 'Successfully deleted project', {
|
|
231
|
+
method: 'deleteProject',
|
|
232
|
+
projectId
|
|
233
|
+
});
|
|
234
|
+
return { success: true };
|
|
235
|
+
} catch (error) {
|
|
236
|
+
mcpLog('error', 'Failed to delete project', {
|
|
237
|
+
method: 'deleteProject',
|
|
238
|
+
projectId,
|
|
239
|
+
error: error.message,
|
|
240
|
+
apiStatus: error.response?.status,
|
|
241
|
+
apiMessage: error.response?.data?.message
|
|
242
|
+
});
|
|
243
|
+
throw new Error(`Failed to delete project: ${error.response?.data?.message || error.message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async getTasks(options = {}) {
|
|
248
|
+
try {
|
|
249
|
+
mcpLog('debug', 'Fetching tasks from Motion API', {
|
|
250
|
+
method: 'getTasks',
|
|
251
|
+
filters: options
|
|
252
|
+
});
|
|
253
|
+
const params = new URLSearchParams();
|
|
254
|
+
if (options.projectId) params.append('projectId', options.projectId);
|
|
255
|
+
if (options.status) params.append('status', options.status);
|
|
256
|
+
if (options.assigneeId) params.append('assigneeId', options.assigneeId);
|
|
257
|
+
|
|
258
|
+
const response = await this.client.get(`/tasks?${params}`);
|
|
259
|
+
|
|
260
|
+
// Handle Motion API response structure - tasks are wrapped in a tasks array
|
|
261
|
+
const tasks = response.data?.tasks || response.data || [];
|
|
262
|
+
|
|
263
|
+
mcpLog('info', 'Successfully fetched tasks', {
|
|
264
|
+
method: 'getTasks',
|
|
265
|
+
count: tasks.length,
|
|
266
|
+
filters: options,
|
|
267
|
+
responseStructure: response.data?.tasks ? 'wrapped' : 'direct'
|
|
268
|
+
});
|
|
269
|
+
return tasks;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
mcpLog('error', 'Failed to fetch tasks', {
|
|
272
|
+
method: 'getTasks',
|
|
273
|
+
filters: options,
|
|
274
|
+
error: error.message,
|
|
275
|
+
apiStatus: error.response?.status,
|
|
276
|
+
apiMessage: error.response?.data?.message
|
|
277
|
+
});
|
|
278
|
+
throw new Error(`Failed to fetch tasks: ${error.response?.data?.message || error.message}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async createTask(taskData) {
|
|
283
|
+
try {
|
|
284
|
+
mcpLog('debug', 'Creating new task in Motion API', {
|
|
285
|
+
method: 'createTask',
|
|
286
|
+
taskName: taskData.name,
|
|
287
|
+
projectId: taskData.projectId,
|
|
288
|
+
workspaceId: taskData.workspaceId
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Ensure workspaceId is present (required by Motion API)
|
|
292
|
+
if (!taskData.workspaceId) {
|
|
293
|
+
try {
|
|
294
|
+
const defaultWorkspace = await this.getDefaultWorkspace();
|
|
295
|
+
taskData = { ...taskData, workspaceId: defaultWorkspace.id };
|
|
296
|
+
mcpLog('info', 'Using default workspace for new task', {
|
|
297
|
+
method: 'createTask',
|
|
298
|
+
workspaceId: defaultWorkspace.id,
|
|
299
|
+
workspaceName: defaultWorkspace.name
|
|
300
|
+
});
|
|
301
|
+
} catch (workspaceError) {
|
|
302
|
+
throw new Error('workspaceId is required to create a task and no default workspace could be found');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Validate required fields according to Motion API
|
|
307
|
+
if (!taskData.name) {
|
|
308
|
+
throw new Error('Task name is required');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Log the final task data being sent to API
|
|
312
|
+
mcpLog('debug', 'Sending task data to Motion API', {
|
|
313
|
+
method: 'createTask',
|
|
314
|
+
taskData: {
|
|
315
|
+
name: taskData.name,
|
|
316
|
+
workspaceId: taskData.workspaceId,
|
|
317
|
+
projectId: taskData.projectId || 'none',
|
|
318
|
+
status: taskData.status || 'default',
|
|
319
|
+
priority: taskData.priority || 'not_set'
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const response = await this.client.post('/tasks', taskData);
|
|
324
|
+
|
|
325
|
+
mcpLog('info', 'Successfully created task', {
|
|
326
|
+
method: 'createTask',
|
|
327
|
+
taskId: response.data?.id,
|
|
328
|
+
taskName: response.data?.name,
|
|
329
|
+
projectId: taskData.projectId,
|
|
330
|
+
workspaceId: taskData.workspaceId
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return response.data;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
mcpLog('error', 'Failed to create task', {
|
|
336
|
+
method: 'createTask',
|
|
337
|
+
taskName: taskData.name,
|
|
338
|
+
projectId: taskData.projectId,
|
|
339
|
+
workspaceId: taskData.workspaceId,
|
|
340
|
+
error: error.message,
|
|
341
|
+
apiStatus: error.response?.status,
|
|
342
|
+
apiMessage: error.response?.data?.message,
|
|
343
|
+
apiErrors: error.response?.data?.errors
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Provide more specific error messages
|
|
347
|
+
if (error.response?.status === 400) {
|
|
348
|
+
const apiMessage = error.response?.data?.message || '';
|
|
349
|
+
if (apiMessage.includes('workspaceId')) {
|
|
350
|
+
throw new Error('Invalid or missing workspaceId. Please provide a valid workspace ID.');
|
|
351
|
+
} else if (apiMessage.includes('projectId')) {
|
|
352
|
+
throw new Error('Invalid projectId. Please check that the project exists in the specified workspace.');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
throw new Error(`Failed to create task: ${error.response?.data?.message || error.message}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async getTask(taskId) {
|
|
361
|
+
try {
|
|
362
|
+
mcpLog('debug', 'Fetching task details from Motion API', {
|
|
363
|
+
method: 'getTask',
|
|
364
|
+
taskId
|
|
365
|
+
});
|
|
366
|
+
const response = await this.client.get(`/tasks/${taskId}`);
|
|
367
|
+
mcpLog('info', 'Successfully fetched task details', {
|
|
368
|
+
method: 'getTask',
|
|
369
|
+
taskId,
|
|
370
|
+
taskName: response.data?.name
|
|
371
|
+
});
|
|
372
|
+
return response.data;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
mcpLog('error', 'Failed to fetch task', {
|
|
375
|
+
method: 'getTask',
|
|
376
|
+
taskId,
|
|
377
|
+
error: error.message,
|
|
378
|
+
apiStatus: error.response?.status,
|
|
379
|
+
apiMessage: error.response?.data?.message
|
|
380
|
+
});
|
|
381
|
+
throw new Error(`Failed to fetch task: ${error.response?.data?.message || error.message}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async updateTask(taskId, taskData) {
|
|
386
|
+
try {
|
|
387
|
+
mcpLog('debug', 'Updating task in Motion API', {
|
|
388
|
+
method: 'updateTask',
|
|
389
|
+
taskId,
|
|
390
|
+
updateFields: Object.keys(taskData)
|
|
391
|
+
});
|
|
392
|
+
const response = await this.client.patch(`/tasks/${taskId}`, taskData);
|
|
393
|
+
mcpLog('info', 'Successfully updated task', {
|
|
394
|
+
method: 'updateTask',
|
|
395
|
+
taskId,
|
|
396
|
+
taskName: response.data?.name
|
|
397
|
+
});
|
|
398
|
+
return response.data;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
mcpLog('error', 'Failed to update task', {
|
|
401
|
+
method: 'updateTask',
|
|
402
|
+
taskId,
|
|
403
|
+
error: error.message,
|
|
404
|
+
apiStatus: error.response?.status,
|
|
405
|
+
apiMessage: error.response?.data?.message
|
|
406
|
+
});
|
|
407
|
+
throw new Error(`Failed to update task: ${error.response?.data?.message || error.message}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async deleteTask(taskId) {
|
|
412
|
+
try {
|
|
413
|
+
mcpLog('debug', 'Deleting task from Motion API', {
|
|
414
|
+
method: 'deleteTask',
|
|
415
|
+
taskId
|
|
416
|
+
});
|
|
417
|
+
await this.client.delete(`/tasks/${taskId}`);
|
|
418
|
+
mcpLog('info', 'Successfully deleted task', {
|
|
419
|
+
method: 'deleteTask',
|
|
420
|
+
taskId
|
|
421
|
+
});
|
|
422
|
+
return { success: true };
|
|
423
|
+
} catch (error) {
|
|
424
|
+
mcpLog('error', 'Failed to delete task', {
|
|
425
|
+
method: 'deleteTask',
|
|
426
|
+
taskId,
|
|
427
|
+
error: error.message,
|
|
428
|
+
apiStatus: error.response?.status,
|
|
429
|
+
apiMessage: error.response?.data?.message
|
|
430
|
+
});
|
|
431
|
+
throw new Error(`Failed to delete task: ${error.response?.data?.message || error.message}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async getWorkspaces() {
|
|
436
|
+
try {
|
|
437
|
+
mcpLog('debug', 'Fetching workspaces from Motion API', { method: 'getWorkspaces' });
|
|
438
|
+
const response = await this.client.get('/workspaces');
|
|
439
|
+
|
|
440
|
+
// Motion API returns workspaces wrapped in a "workspaces" property
|
|
441
|
+
let workspaces = response.data;
|
|
442
|
+
|
|
443
|
+
if (workspaces && workspaces.workspaces && Array.isArray(workspaces.workspaces)) {
|
|
444
|
+
// Expected structure: { workspaces: [...] }
|
|
445
|
+
workspaces = workspaces.workspaces;
|
|
446
|
+
mcpLog('info', 'Successfully fetched workspaces', {
|
|
447
|
+
method: 'getWorkspaces',
|
|
448
|
+
count: workspaces.length,
|
|
449
|
+
responseStructure: 'wrapped_array',
|
|
450
|
+
workspaceNames: workspaces.map(w => w.name)
|
|
451
|
+
});
|
|
452
|
+
} else if (Array.isArray(workspaces)) {
|
|
453
|
+
// Fallback: if it's already an array
|
|
454
|
+
mcpLog('info', 'Successfully fetched workspaces', {
|
|
455
|
+
method: 'getWorkspaces',
|
|
456
|
+
count: workspaces.length,
|
|
457
|
+
responseStructure: 'direct_array',
|
|
458
|
+
workspaceNames: workspaces.map(w => w.name)
|
|
459
|
+
});
|
|
460
|
+
} else {
|
|
461
|
+
// Unexpected structure
|
|
462
|
+
mcpLog('warn', 'Unexpected workspace response structure', {
|
|
463
|
+
method: 'getWorkspaces',
|
|
464
|
+
responseData: workspaces,
|
|
465
|
+
responseType: typeof workspaces
|
|
466
|
+
});
|
|
467
|
+
workspaces = [];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return workspaces;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
mcpLog('error', 'Failed to fetch workspaces', {
|
|
473
|
+
method: 'getWorkspaces',
|
|
474
|
+
error: error.message,
|
|
475
|
+
apiStatus: error.response?.status,
|
|
476
|
+
apiMessage: error.response?.data?.message
|
|
477
|
+
});
|
|
478
|
+
throw new Error(`Failed to fetch workspaces: ${error.response?.data?.message || error.message}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async getUsers() {
|
|
483
|
+
try {
|
|
484
|
+
mcpLog('debug', 'Fetching users from Motion API', { method: 'getUsers' });
|
|
485
|
+
const response = await this.client.get('/users');
|
|
486
|
+
|
|
487
|
+
// Handle different response structures from Motion API
|
|
488
|
+
let users = response.data;
|
|
489
|
+
|
|
490
|
+
// If response.data is not an array, check if it's wrapped in a property
|
|
491
|
+
if (!Array.isArray(users)) {
|
|
492
|
+
if (users && users.users && Array.isArray(users.users)) {
|
|
493
|
+
users = users.users;
|
|
494
|
+
} else if (users && typeof users === 'object') {
|
|
495
|
+
// If it's a single user object, wrap it in an array
|
|
496
|
+
users = [users];
|
|
497
|
+
} else {
|
|
498
|
+
// If we can't determine the structure, log it and return empty array
|
|
499
|
+
mcpLog('warn', 'Unexpected users response structure', {
|
|
500
|
+
method: 'getUsers',
|
|
501
|
+
responseData: users,
|
|
502
|
+
responseType: typeof users
|
|
503
|
+
});
|
|
504
|
+
users = [];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
mcpLog('info', 'Successfully fetched users', {
|
|
509
|
+
method: 'getUsers',
|
|
510
|
+
count: users.length,
|
|
511
|
+
responseStructure: Array.isArray(response.data) ? 'array' : 'object'
|
|
512
|
+
});
|
|
513
|
+
return users;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
mcpLog('error', 'Failed to fetch users', {
|
|
516
|
+
method: 'getUsers',
|
|
517
|
+
error: error.message,
|
|
518
|
+
apiStatus: error.response?.status,
|
|
519
|
+
apiMessage: error.response?.data?.message
|
|
520
|
+
});
|
|
521
|
+
throw new Error(`Failed to fetch users: ${error.response?.data?.message || error.message}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async getDefaultWorkspace() {
|
|
526
|
+
try {
|
|
527
|
+
const workspaces = await this.getWorkspaces();
|
|
528
|
+
|
|
529
|
+
if (!workspaces || workspaces.length === 0) {
|
|
530
|
+
throw new Error('No workspaces available');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Log all available workspaces for debugging
|
|
534
|
+
mcpLog('debug', 'Available workspaces', {
|
|
535
|
+
method: 'getDefaultWorkspace',
|
|
536
|
+
workspaces: workspaces.map(w => ({ id: w.id, name: w.name, type: w.type }))
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Prefer the first workspace, but could add logic here to prefer certain types
|
|
540
|
+
// For example, prefer "INDIVIDUAL" type workspaces over team workspaces
|
|
541
|
+
let defaultWorkspace = workspaces[0];
|
|
542
|
+
|
|
543
|
+
// Look for a personal or individual workspace first
|
|
544
|
+
const personalWorkspace = workspaces.find(w =>
|
|
545
|
+
w.type === 'INDIVIDUAL' &&
|
|
546
|
+
(w.name.toLowerCase().includes('personal') || w.name.toLowerCase().includes('my'))
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
if (personalWorkspace) {
|
|
550
|
+
defaultWorkspace = personalWorkspace;
|
|
551
|
+
mcpLog('info', 'Selected personal workspace as default', {
|
|
552
|
+
method: 'getDefaultWorkspace',
|
|
553
|
+
workspaceId: defaultWorkspace.id,
|
|
554
|
+
workspaceName: defaultWorkspace.name,
|
|
555
|
+
type: defaultWorkspace.type
|
|
556
|
+
});
|
|
557
|
+
} else {
|
|
558
|
+
mcpLog('info', 'Selected first available workspace as default', {
|
|
559
|
+
method: 'getDefaultWorkspace',
|
|
560
|
+
workspaceId: defaultWorkspace.id,
|
|
561
|
+
workspaceName: defaultWorkspace.name,
|
|
562
|
+
type: defaultWorkspace.type
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return defaultWorkspace;
|
|
567
|
+
} catch (error) {
|
|
568
|
+
mcpLog('error', 'Failed to get default workspace', {
|
|
569
|
+
method: 'getDefaultWorkspace',
|
|
570
|
+
error: error.message
|
|
571
|
+
});
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async getWorkspaceByName(workspaceName) {
|
|
577
|
+
try {
|
|
578
|
+
const workspaces = await this.getWorkspaces();
|
|
579
|
+
const workspace = workspaces.find(w => w.name.toLowerCase() === workspaceName.toLowerCase());
|
|
580
|
+
|
|
581
|
+
if (!workspace) {
|
|
582
|
+
throw new Error(`Workspace with name "${workspaceName}" not found`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
mcpLog('info', 'Found workspace by name', {
|
|
586
|
+
method: 'getWorkspaceByName',
|
|
587
|
+
workspaceName,
|
|
588
|
+
workspaceId: workspace.id
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
return workspace;
|
|
592
|
+
} catch (error) {
|
|
593
|
+
mcpLog('error', 'Failed to find workspace by name', {
|
|
594
|
+
method: 'getWorkspaceByName',
|
|
595
|
+
workspaceName,
|
|
596
|
+
error: error.message
|
|
597
|
+
});
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async getTaskStatuses(workspaceId = null) {
|
|
603
|
+
try {
|
|
604
|
+
if (!workspaceId) {
|
|
605
|
+
const defaultWorkspace = await this.getDefaultWorkspace();
|
|
606
|
+
workspaceId = defaultWorkspace.id;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const workspaces = await this.getWorkspaces();
|
|
610
|
+
const workspace = workspaces.find(w => w.id === workspaceId);
|
|
611
|
+
|
|
612
|
+
if (!workspace) {
|
|
613
|
+
throw new Error(`Workspace with ID "${workspaceId}" not found`);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
mcpLog('info', 'Retrieved task statuses for workspace', {
|
|
617
|
+
method: 'getTaskStatuses',
|
|
618
|
+
workspaceId,
|
|
619
|
+
workspaceName: workspace.name,
|
|
620
|
+
statusCount: workspace.taskStatuses?.length || 0
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
return workspace.taskStatuses || [];
|
|
624
|
+
} catch (error) {
|
|
625
|
+
mcpLog('error', 'Failed to get task statuses', {
|
|
626
|
+
method: 'getTaskStatuses',
|
|
627
|
+
workspaceId,
|
|
628
|
+
error: error.message
|
|
629
|
+
});
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async getWorkspaceLabels(workspaceId = null) {
|
|
635
|
+
try {
|
|
636
|
+
if (!workspaceId) {
|
|
637
|
+
const defaultWorkspace = await this.getDefaultWorkspace();
|
|
638
|
+
workspaceId = defaultWorkspace.id;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const workspaces = await this.getWorkspaces();
|
|
642
|
+
const workspace = workspaces.find(w => w.id === workspaceId);
|
|
643
|
+
|
|
644
|
+
if (!workspace) {
|
|
645
|
+
throw new Error(`Workspace with ID "${workspaceId}" not found`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
mcpLog('info', 'Retrieved labels for workspace', {
|
|
649
|
+
method: 'getWorkspaceLabels',
|
|
650
|
+
workspaceId,
|
|
651
|
+
workspaceName: workspace.name,
|
|
652
|
+
labelCount: workspace.labels?.length || 0
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
return workspace.labels || [];
|
|
656
|
+
} catch (error) {
|
|
657
|
+
mcpLog('error', 'Failed to get workspace labels', {
|
|
658
|
+
method: 'getWorkspaceLabels',
|
|
659
|
+
workspaceId,
|
|
660
|
+
error: error.message
|
|
661
|
+
});
|
|
662
|
+
throw error;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async getProjectByName(projectName, workspaceId = null) {
|
|
667
|
+
try {
|
|
668
|
+
if (!workspaceId) {
|
|
669
|
+
const defaultWorkspace = await this.getDefaultWorkspace();
|
|
670
|
+
workspaceId = defaultWorkspace.id;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const projects = await this.getProjects(workspaceId);
|
|
674
|
+
const project = projects.find(p =>
|
|
675
|
+
p.name.toLowerCase() === projectName.toLowerCase() ||
|
|
676
|
+
p.name.toLowerCase().includes(projectName.toLowerCase())
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
if (!project) {
|
|
680
|
+
throw new Error(`Project "${projectName}" not found in workspace`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
mcpLog('info', 'Found project by name', {
|
|
684
|
+
method: 'getProjectByName',
|
|
685
|
+
projectName,
|
|
686
|
+
projectId: project.id,
|
|
687
|
+
workspaceId
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
return project;
|
|
691
|
+
} catch (error) {
|
|
692
|
+
mcpLog('error', 'Failed to find project by name', {
|
|
693
|
+
method: 'getProjectByName',
|
|
694
|
+
projectName,
|
|
695
|
+
workspaceId,
|
|
696
|
+
error: error.message
|
|
697
|
+
});
|
|
698
|
+
throw error;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Enhanced Intelligence Methods
|
|
703
|
+
|
|
704
|
+
async getMotionContext(options = {}) {
|
|
705
|
+
try {
|
|
706
|
+
const {
|
|
707
|
+
includeRecentActivity = true,
|
|
708
|
+
includeWorkloadSummary = true,
|
|
709
|
+
includeSuggestions = true
|
|
710
|
+
} = options;
|
|
711
|
+
|
|
712
|
+
mcpLog('info', 'Fetching Motion context', {
|
|
713
|
+
method: 'getMotionContext',
|
|
714
|
+
includeRecentActivity,
|
|
715
|
+
includeWorkloadSummary,
|
|
716
|
+
includeSuggestions
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const context = {
|
|
720
|
+
timestamp: new Date().toISOString(),
|
|
721
|
+
user: null,
|
|
722
|
+
defaultWorkspace: null,
|
|
723
|
+
workspaces: [],
|
|
724
|
+
recentActivity: [],
|
|
725
|
+
workloadSummary: {},
|
|
726
|
+
suggestions: []
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// Get user info and workspaces
|
|
730
|
+
try {
|
|
731
|
+
const [users, workspaces] = await Promise.all([
|
|
732
|
+
this.getUsers(),
|
|
733
|
+
this.getWorkspaces()
|
|
734
|
+
]);
|
|
735
|
+
|
|
736
|
+
context.user = users[0] || null; // Assume first user is current user
|
|
737
|
+
context.workspaces = workspaces;
|
|
738
|
+
context.defaultWorkspace = await this.getDefaultWorkspace();
|
|
739
|
+
} catch (error) {
|
|
740
|
+
mcpLog('warn', 'Could not fetch user or workspace info for context', {
|
|
741
|
+
method: 'getMotionContext',
|
|
742
|
+
error: error.message
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Get recent activity if requested
|
|
747
|
+
if (includeRecentActivity && context.defaultWorkspace) {
|
|
748
|
+
try {
|
|
749
|
+
const recentTasks = await this.getTasks({
|
|
750
|
+
workspaceId: context.defaultWorkspace.id,
|
|
751
|
+
limit: 10
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
context.recentActivity = recentTasks
|
|
755
|
+
.sort((a, b) => new Date(b.updatedAt || b.createdAt) - new Date(a.updatedAt || a.createdAt))
|
|
756
|
+
.slice(0, 5)
|
|
757
|
+
.map(task => ({
|
|
758
|
+
type: 'task',
|
|
759
|
+
id: task.id,
|
|
760
|
+
name: task.name,
|
|
761
|
+
status: task.status,
|
|
762
|
+
priority: task.priority,
|
|
763
|
+
updatedAt: task.updatedAt || task.createdAt
|
|
764
|
+
}));
|
|
765
|
+
} catch (error) {
|
|
766
|
+
mcpLog('warn', 'Could not fetch recent activity for context', {
|
|
767
|
+
method: 'getMotionContext',
|
|
768
|
+
error: error.message
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Generate workload summary if requested
|
|
774
|
+
if (includeWorkloadSummary && context.defaultWorkspace) {
|
|
775
|
+
try {
|
|
776
|
+
context.workloadSummary = await this.generateWorkloadSummary(context.defaultWorkspace.id);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
mcpLog('warn', 'Could not generate workload summary for context', {
|
|
779
|
+
method: 'getMotionContext',
|
|
780
|
+
error: error.message
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Generate suggestions if requested
|
|
786
|
+
if (includeSuggestions && context.defaultWorkspace) {
|
|
787
|
+
try {
|
|
788
|
+
context.suggestions = await this.generateContextSuggestions(context.defaultWorkspace.id);
|
|
789
|
+
} catch (error) {
|
|
790
|
+
mcpLog('warn', 'Could not generate suggestions for context', {
|
|
791
|
+
method: 'getMotionContext',
|
|
792
|
+
error: error.message
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
mcpLog('info', 'Successfully generated Motion context', {
|
|
798
|
+
method: 'getMotionContext',
|
|
799
|
+
workspaceCount: context.workspaces.length,
|
|
800
|
+
recentActivityCount: context.recentActivity.length,
|
|
801
|
+
suggestionsCount: context.suggestions.length
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
return context;
|
|
805
|
+
} catch (error) {
|
|
806
|
+
mcpLog('error', 'Failed to get Motion context', {
|
|
807
|
+
method: 'getMotionContext',
|
|
808
|
+
error: error.message
|
|
809
|
+
});
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async searchContent(options) {
|
|
815
|
+
try {
|
|
816
|
+
const { query, searchScope = "both", workspaceId, limit = 20 } = options;
|
|
817
|
+
|
|
818
|
+
mcpLog('info', 'Searching Motion content', {
|
|
819
|
+
method: 'searchContent',
|
|
820
|
+
query,
|
|
821
|
+
searchScope,
|
|
822
|
+
workspaceId,
|
|
823
|
+
limit
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
let results = [];
|
|
827
|
+
|
|
828
|
+
// Search tasks if scope includes tasks
|
|
829
|
+
if (searchScope === "tasks" || searchScope === "both") {
|
|
830
|
+
try {
|
|
831
|
+
const tasks = await this.getTasks({ workspaceId });
|
|
832
|
+
const taskResults = tasks
|
|
833
|
+
.filter(task =>
|
|
834
|
+
task.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
835
|
+
(task.description && task.description.toLowerCase().includes(query.toLowerCase()))
|
|
836
|
+
)
|
|
837
|
+
.map(task => ({
|
|
838
|
+
...task,
|
|
839
|
+
type: 'task',
|
|
840
|
+
relevance: this.calculateRelevance(task, query)
|
|
841
|
+
}));
|
|
842
|
+
|
|
843
|
+
results.push(...taskResults);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
mcpLog('warn', 'Error searching tasks', {
|
|
846
|
+
method: 'searchContent',
|
|
847
|
+
error: error.message
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Search projects if scope includes projects
|
|
853
|
+
if (searchScope === "projects" || searchScope === "both") {
|
|
854
|
+
try {
|
|
855
|
+
const projects = await this.getProjects(workspaceId);
|
|
856
|
+
const projectResults = projects
|
|
857
|
+
.filter(project =>
|
|
858
|
+
project.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
859
|
+
(project.description && project.description.toLowerCase().includes(query.toLowerCase()))
|
|
860
|
+
)
|
|
861
|
+
.map(project => ({
|
|
862
|
+
...project,
|
|
863
|
+
type: 'project',
|
|
864
|
+
relevance: this.calculateRelevance(project, query)
|
|
865
|
+
}));
|
|
866
|
+
|
|
867
|
+
results.push(...projectResults);
|
|
868
|
+
} catch (error) {
|
|
869
|
+
mcpLog('warn', 'Error searching projects', {
|
|
870
|
+
method: 'searchContent',
|
|
871
|
+
error: error.message
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Sort by relevance and limit results
|
|
877
|
+
results = results
|
|
878
|
+
.sort((a, b) => b.relevance - a.relevance)
|
|
879
|
+
.slice(0, limit);
|
|
880
|
+
|
|
881
|
+
mcpLog('info', 'Search completed', {
|
|
882
|
+
method: 'searchContent',
|
|
883
|
+
query,
|
|
884
|
+
resultsCount: results.length
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
return results;
|
|
888
|
+
} catch (error) {
|
|
889
|
+
mcpLog('error', 'Failed to search content', {
|
|
890
|
+
method: 'searchContent',
|
|
891
|
+
error: error.message
|
|
892
|
+
});
|
|
893
|
+
throw error;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
calculateRelevance(item, query) {
|
|
898
|
+
const queryLower = query.toLowerCase();
|
|
899
|
+
let score = 0;
|
|
900
|
+
|
|
901
|
+
// Exact name match gets highest score
|
|
902
|
+
if (item.name.toLowerCase() === queryLower) {
|
|
903
|
+
score += 100;
|
|
904
|
+
}
|
|
905
|
+
// Name contains query
|
|
906
|
+
else if (item.name.toLowerCase().includes(queryLower)) {
|
|
907
|
+
score += 50;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Description contains query
|
|
911
|
+
if (item.description && item.description.toLowerCase().includes(queryLower)) {
|
|
912
|
+
score += 25;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Boost score for high priority items
|
|
916
|
+
if (item.priority === 'ASAP') score += 20;
|
|
917
|
+
else if (item.priority === 'HIGH') score += 10;
|
|
918
|
+
|
|
919
|
+
return score;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
async analyzeWorkload(options) {
|
|
923
|
+
try {
|
|
924
|
+
const { workspaceId, timeframe = "this_week", includeProjects = true } = options;
|
|
925
|
+
|
|
926
|
+
mcpLog('info', 'Analyzing workload', {
|
|
927
|
+
method: 'analyzeWorkload',
|
|
928
|
+
workspaceId,
|
|
929
|
+
timeframe,
|
|
930
|
+
includeProjects
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
const wsId = workspaceId || (await this.getDefaultWorkspace()).id;
|
|
934
|
+
const tasks = await this.getTasks({ workspaceId: wsId });
|
|
935
|
+
|
|
936
|
+
const now = new Date();
|
|
937
|
+
const analysis = {
|
|
938
|
+
totalTasks: tasks.length,
|
|
939
|
+
overdueTasks: 0,
|
|
940
|
+
upcomingDeadlines: 0,
|
|
941
|
+
taskDistribution: {
|
|
942
|
+
byStatus: {},
|
|
943
|
+
byPriority: {},
|
|
944
|
+
byProject: {}
|
|
945
|
+
},
|
|
946
|
+
projectInsights: {}
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// Analyze tasks
|
|
950
|
+
tasks.forEach(task => {
|
|
951
|
+
// Count overdue tasks
|
|
952
|
+
if (task.dueDate && new Date(task.dueDate) < now) {
|
|
953
|
+
analysis.overdueTasks++;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Count upcoming deadlines (next 7 days)
|
|
957
|
+
if (task.dueDate) {
|
|
958
|
+
const dueDate = new Date(task.dueDate);
|
|
959
|
+
const daysUntilDue = (dueDate - now) / (1000 * 60 * 60 * 24);
|
|
960
|
+
if (daysUntilDue >= 0 && daysUntilDue <= 7) {
|
|
961
|
+
analysis.upcomingDeadlines++;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Task distribution by status
|
|
966
|
+
analysis.taskDistribution.byStatus[task.status] =
|
|
967
|
+
(analysis.taskDistribution.byStatus[task.status] || 0) + 1;
|
|
968
|
+
|
|
969
|
+
// Task distribution by priority
|
|
970
|
+
analysis.taskDistribution.byPriority[task.priority || 'NONE'] =
|
|
971
|
+
(analysis.taskDistribution.byPriority[task.priority || 'NONE'] || 0) + 1;
|
|
972
|
+
|
|
973
|
+
// Task distribution by project
|
|
974
|
+
const projectKey = task.projectId || 'No Project';
|
|
975
|
+
analysis.taskDistribution.byProject[projectKey] =
|
|
976
|
+
(analysis.taskDistribution.byProject[projectKey] || 0) + 1;
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
// Project insights if requested
|
|
980
|
+
if (includeProjects) {
|
|
981
|
+
try {
|
|
982
|
+
const projects = await this.getProjects(wsId);
|
|
983
|
+
analysis.projectInsights = {
|
|
984
|
+
totalProjects: projects.length,
|
|
985
|
+
activeProjects: projects.filter(p => p.status === 'active').length,
|
|
986
|
+
completedProjects: projects.filter(p => p.status === 'completed').length
|
|
987
|
+
};
|
|
988
|
+
} catch (error) {
|
|
989
|
+
mcpLog('warn', 'Could not fetch project insights', {
|
|
990
|
+
method: 'analyzeWorkload',
|
|
991
|
+
error: error.message
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
mcpLog('info', 'Workload analysis completed', {
|
|
997
|
+
method: 'analyzeWorkload',
|
|
998
|
+
totalTasks: analysis.totalTasks,
|
|
999
|
+
overdueTasks: analysis.overdueTasks,
|
|
1000
|
+
upcomingDeadlines: analysis.upcomingDeadlines
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
return analysis;
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
mcpLog('error', 'Failed to analyze workload', {
|
|
1006
|
+
method: 'analyzeWorkload',
|
|
1007
|
+
error: error.message
|
|
1008
|
+
});
|
|
1009
|
+
throw error;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async generateWorkloadSummary(workspaceId) {
|
|
1014
|
+
const tasks = await this.getTasks({ workspaceId });
|
|
1015
|
+
const now = new Date();
|
|
1016
|
+
|
|
1017
|
+
return {
|
|
1018
|
+
total: tasks.length,
|
|
1019
|
+
completed: tasks.filter(t => t.status === 'completed' || t.status === 'done').length,
|
|
1020
|
+
inProgress: tasks.filter(t => t.status === 'in-progress' || t.status === 'in_progress').length,
|
|
1021
|
+
overdue: tasks.filter(t => t.dueDate && new Date(t.dueDate) < now).length,
|
|
1022
|
+
highPriority: tasks.filter(t => t.priority === 'ASAP' || t.priority === 'HIGH').length
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async generateContextSuggestions(workspaceId) {
|
|
1027
|
+
const tasks = await this.getTasks({ workspaceId });
|
|
1028
|
+
const suggestions = [];
|
|
1029
|
+
|
|
1030
|
+
// Find overdue tasks
|
|
1031
|
+
const overdueTasks = tasks.filter(t =>
|
|
1032
|
+
t.dueDate && new Date(t.dueDate) < new Date()
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
if (overdueTasks.length > 0) {
|
|
1036
|
+
suggestions.push(`You have ${overdueTasks.length} overdue task(s) that need attention`);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Find high priority tasks
|
|
1040
|
+
const highPriorityTasks = tasks.filter(t =>
|
|
1041
|
+
t.priority === 'ASAP' || t.priority === 'HIGH'
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
if (highPriorityTasks.length > 0) {
|
|
1045
|
+
suggestions.push(`Consider focusing on ${highPriorityTasks.length} high-priority task(s)`);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Find tasks due soon
|
|
1049
|
+
const soon = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours from now
|
|
1050
|
+
const tasksDueSoon = tasks.filter(t =>
|
|
1051
|
+
t.dueDate && new Date(t.dueDate) <= soon && new Date(t.dueDate) >= new Date()
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
if (tasksDueSoon.length > 0) {
|
|
1055
|
+
suggestions.push(`${tasksDueSoon.length} task(s) are due within 24 hours`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return suggestions;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async bulkUpdateTasks(taskIds, updates) {
|
|
1062
|
+
try {
|
|
1063
|
+
mcpLog('info', 'Starting bulk task update', {
|
|
1064
|
+
method: 'bulkUpdateTasks',
|
|
1065
|
+
taskCount: taskIds.length,
|
|
1066
|
+
updates
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
const updatePromises = taskIds.map(taskId =>
|
|
1070
|
+
this.updateTask(taskId, updates)
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
const results = await Promise.allSettled(updatePromises);
|
|
1074
|
+
const successful = results.filter(r => r.status === 'fulfilled').length;
|
|
1075
|
+
const failed = results.filter(r => r.status === 'rejected').length;
|
|
1076
|
+
|
|
1077
|
+
mcpLog('info', 'Bulk task update completed', {
|
|
1078
|
+
method: 'bulkUpdateTasks',
|
|
1079
|
+
successful,
|
|
1080
|
+
failed,
|
|
1081
|
+
total: taskIds.length
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
if (failed > 0) {
|
|
1085
|
+
const failures = results
|
|
1086
|
+
.filter(r => r.status === 'rejected')
|
|
1087
|
+
.map(r => r.reason.message);
|
|
1088
|
+
|
|
1089
|
+
throw new Error(`${failed} task updates failed: ${failures.join(', ')}`);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return { successful, failed };
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
mcpLog('error', 'Failed to bulk update tasks', {
|
|
1095
|
+
method: 'bulkUpdateTasks',
|
|
1096
|
+
error: error.message
|
|
1097
|
+
});
|
|
1098
|
+
throw error;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async smartScheduleTasks(taskIds, workspaceId, preferences = {}) {
|
|
1103
|
+
try {
|
|
1104
|
+
mcpLog('info', 'Starting smart task scheduling', {
|
|
1105
|
+
method: 'smartScheduleTasks',
|
|
1106
|
+
taskCount: taskIds?.length || 0,
|
|
1107
|
+
workspaceId,
|
|
1108
|
+
preferences
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
// Get tasks to schedule
|
|
1112
|
+
let tasksToSchedule = [];
|
|
1113
|
+
if (taskIds && taskIds.length > 0) {
|
|
1114
|
+
// Get specific tasks
|
|
1115
|
+
const taskPromises = taskIds.map(id => this.getTask(id));
|
|
1116
|
+
const taskResults = await Promise.allSettled(taskPromises);
|
|
1117
|
+
tasksToSchedule = taskResults
|
|
1118
|
+
.filter(r => r.status === 'fulfilled')
|
|
1119
|
+
.map(r => r.value);
|
|
1120
|
+
} else {
|
|
1121
|
+
// Get all unscheduled tasks in workspace
|
|
1122
|
+
const allTasks = await this.getTasks({ workspaceId });
|
|
1123
|
+
tasksToSchedule = allTasks.filter(task =>
|
|
1124
|
+
!task.scheduledStart || task.status === 'todo'
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Sort tasks by priority and deadline
|
|
1129
|
+
const sortedTasks = tasksToSchedule.sort((a, b) => {
|
|
1130
|
+
// Priority order: ASAP > HIGH > MEDIUM > LOW
|
|
1131
|
+
const priorityOrder = { 'ASAP': 4, 'HIGH': 3, 'MEDIUM': 2, 'LOW': 1 };
|
|
1132
|
+
const aPriority = priorityOrder[a.priority] || 1;
|
|
1133
|
+
const bPriority = priorityOrder[b.priority] || 1;
|
|
1134
|
+
|
|
1135
|
+
if (aPriority !== bPriority) {
|
|
1136
|
+
return bPriority - aPriority; // Higher priority first
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// If same priority, sort by due date
|
|
1140
|
+
if (a.dueDate && b.dueDate) {
|
|
1141
|
+
return new Date(a.dueDate) - new Date(b.dueDate);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return 0;
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
// Generate schedule (simplified - in reality, this would use Motion's auto-scheduling)
|
|
1148
|
+
const schedule = sortedTasks.map((task, index) => {
|
|
1149
|
+
const startTime = new Date();
|
|
1150
|
+
startTime.setHours(9 + index, 0, 0, 0); // Start at 9 AM, one hour apart
|
|
1151
|
+
|
|
1152
|
+
return {
|
|
1153
|
+
taskId: task.id,
|
|
1154
|
+
taskName: task.name,
|
|
1155
|
+
scheduledTime: startTime.toISOString(),
|
|
1156
|
+
priority: task.priority,
|
|
1157
|
+
estimatedDuration: task.duration || 60
|
|
1158
|
+
};
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
mcpLog('info', 'Smart scheduling completed', {
|
|
1162
|
+
method: 'smartScheduleTasks',
|
|
1163
|
+
scheduledCount: schedule.length
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
return schedule;
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
mcpLog('error', 'Failed to smart schedule tasks', {
|
|
1169
|
+
method: 'smartScheduleTasks',
|
|
1170
|
+
error: error.message
|
|
1171
|
+
});
|
|
1172
|
+
throw error;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
module.exports = MotionApiService;
|