motionmcp 2.2.4 → 2.4.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 (46) hide show
  1. package/README.md +121 -164
  2. package/dist/handlers/ProjectHandler.js +4 -4
  3. package/dist/handlers/ProjectHandler.js.map +1 -1
  4. package/dist/handlers/RecurringTaskHandler.js +2 -2
  5. package/dist/handlers/RecurringTaskHandler.js.map +1 -1
  6. package/dist/handlers/SearchHandler.d.ts.map +1 -1
  7. package/dist/handlers/SearchHandler.js +20 -4
  8. package/dist/handlers/SearchHandler.js.map +1 -1
  9. package/dist/handlers/TaskHandler.d.ts.map +1 -1
  10. package/dist/handlers/TaskHandler.js +31 -5
  11. package/dist/handlers/TaskHandler.js.map +1 -1
  12. package/dist/mcp-server-old.d.ts +29 -0
  13. package/dist/mcp-server-old.d.ts.map +1 -0
  14. package/dist/mcp-server-old.js +1304 -0
  15. package/dist/mcp-server-old.js.map +1 -0
  16. package/dist/mcp-server.js +0 -0
  17. package/dist/services/motionApi.d.ts +11 -9
  18. package/dist/services/motionApi.d.ts.map +1 -1
  19. package/dist/services/motionApi.js +182 -120
  20. package/dist/services/motionApi.js.map +1 -1
  21. package/dist/tools/ToolDefinitions.d.ts.map +1 -1
  22. package/dist/tools/ToolDefinitions.js +9 -2
  23. package/dist/tools/ToolDefinitions.js.map +1 -1
  24. package/dist/types/mcp-tool-args.d.ts +2 -1
  25. package/dist/types/mcp-tool-args.d.ts.map +1 -1
  26. package/dist/types/mcp.d.ts +10 -0
  27. package/dist/types/mcp.d.ts.map +1 -1
  28. package/dist/utils/index.d.ts +1 -0
  29. package/dist/utils/index.d.ts.map +1 -1
  30. package/dist/utils/jsonSchemaToZod.d.ts +33 -0
  31. package/dist/utils/jsonSchemaToZod.d.ts.map +1 -0
  32. package/dist/utils/jsonSchemaToZod.js +110 -0
  33. package/dist/utils/jsonSchemaToZod.js.map +1 -0
  34. package/dist/utils/paginationNew.d.ts +2 -0
  35. package/dist/utils/paginationNew.d.ts.map +1 -1
  36. package/dist/utils/paginationNew.js +10 -1
  37. package/dist/utils/paginationNew.js.map +1 -1
  38. package/dist/utils/responseFormatters.d.ts +9 -1
  39. package/dist/utils/responseFormatters.d.ts.map +1 -1
  40. package/dist/utils/responseFormatters.js +33 -7
  41. package/dist/utils/responseFormatters.js.map +1 -1
  42. package/dist/worker.d.ts +17 -0
  43. package/dist/worker.d.ts.map +1 -0
  44. package/dist/worker.js +61 -0
  45. package/dist/worker.js.map +1 -0
  46. package/package.json +6 -2
@@ -0,0 +1,1304 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
38
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
39
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
40
+ const motionApi_1 = require("./services/motionApi");
41
+ const utils_1 = require("./utils");
42
+ const sanitize_1 = require("./utils/sanitize");
43
+ const validator_1 = require("./utils/validator");
44
+ const dotenv = __importStar(require("dotenv"));
45
+ dotenv.config();
46
+ class MotionMCPServer {
47
+ constructor() {
48
+ this.server = new index_js_1.Server({
49
+ name: "motion-mcp-server",
50
+ version: "1.0.0",
51
+ }, {
52
+ capabilities: {
53
+ tools: {},
54
+ },
55
+ });
56
+ this.motionService = null;
57
+ this.workspaceResolver = null;
58
+ this.validator = new validator_1.InputValidator();
59
+ this.toolsConfig = process.env.MOTION_MCP_TOOLS || 'essential';
60
+ this.setupHandlers();
61
+ }
62
+ async initialize() {
63
+ try {
64
+ // Validate tools configuration
65
+ this.validateToolsConfig();
66
+ this.motionService = new motionApi_1.MotionApiService();
67
+ this.workspaceResolver = new utils_1.WorkspaceResolver(this.motionService);
68
+ // Initialize validators with tool definitions
69
+ this.validator.initializeValidators(this.getEnabledTools());
70
+ }
71
+ catch (error) {
72
+ const errorMessage = error instanceof Error ? error.message : String(error);
73
+ (0, utils_1.mcpLog)(utils_1.LOG_LEVELS.ERROR, "Failed to initialize Motion API service", { error: errorMessage });
74
+ process.exit(1);
75
+ }
76
+ }
77
+ validateToolsConfig() {
78
+ const validConfigs = ['minimal', 'essential', 'complete'];
79
+ // Check if it's a valid preset or custom configuration
80
+ if (!validConfigs.includes(this.toolsConfig) && !this.toolsConfig.startsWith('custom:')) {
81
+ (0, utils_1.mcpLog)(utils_1.LOG_LEVELS.ERROR, `Invalid MOTION_MCP_TOOLS configuration: "${this.toolsConfig}"`, {
82
+ validOptions: [...validConfigs, 'custom:tool1,tool2,...'],
83
+ defaulting: 'essential'
84
+ });
85
+ // Still default to essential, but log it prominently
86
+ (0, utils_1.mcpLog)(utils_1.LOG_LEVELS.WARN, 'Defaulting to "essential" configuration');
87
+ this.toolsConfig = 'essential';
88
+ }
89
+ // For custom configurations, validate tool names exist
90
+ if (this.toolsConfig.startsWith('custom:')) {
91
+ const customTools = this.toolsConfig.substring(7).split(',').map(s => s.trim());
92
+ const allToolNames = this.getAllToolDefinitions().map(t => t.name);
93
+ const invalidTools = customTools.filter(name => !allToolNames.includes(name));
94
+ if (invalidTools.length > 0) {
95
+ (0, utils_1.mcpLog)(utils_1.LOG_LEVELS.ERROR, 'Invalid tool names in custom configuration', {
96
+ invalidTools,
97
+ availableTools: allToolNames
98
+ });
99
+ throw new Error(`Invalid tool names in custom configuration: ${invalidTools.join(', ')}`);
100
+ }
101
+ if (customTools.length === 0) {
102
+ throw new Error('Custom configuration must specify at least one tool');
103
+ }
104
+ }
105
+ (0, utils_1.mcpLog)(utils_1.LOG_LEVELS.INFO, `Tool configuration validated: ${this.toolsConfig}`);
106
+ }
107
+ setupHandlers() {
108
+ this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
109
+ return {
110
+ tools: this.getEnabledTools()
111
+ };
112
+ });
113
+ this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
114
+ if (!this.motionService || !this.workspaceResolver) {
115
+ return (0, utils_1.formatMcpError)(new Error("Server not initialized"));
116
+ }
117
+ try {
118
+ const { name, arguments: args } = request.params;
119
+ // Runtime validation
120
+ const validation = this.validator.validateInput(name, args);
121
+ if (!validation.valid) {
122
+ return (0, utils_1.formatMcpError)(new Error(`Invalid arguments for ${name}: ${validation.errors}`));
123
+ }
124
+ switch (name) {
125
+ // Consolidated tools
126
+ case "motion_projects":
127
+ return await this.handleMotionProjects(args);
128
+ case "motion_tasks":
129
+ return await this.handleMotionTasks(args);
130
+ case "motion_workspaces":
131
+ return await this.handleMotionWorkspaces(args);
132
+ case "motion_users":
133
+ return await this.handleMotionUsers(args);
134
+ case "motion_search":
135
+ return await this.handleMotionSearch(args);
136
+ case "motion_comments":
137
+ return await this.handleMotionComments(args);
138
+ case "motion_custom_fields":
139
+ return await this.handleMotionCustomFields(args);
140
+ case "motion_recurring_tasks":
141
+ return await this.handleMotionRecurringTasks(args);
142
+ case "motion_schedules":
143
+ return await this.handleMotionSchedules(args);
144
+ case "motion_statuses":
145
+ return await this.handleMotionStatuses(args);
146
+ default:
147
+ return (0, utils_1.formatMcpError)(new Error(`Unknown tool: ${name}`));
148
+ }
149
+ }
150
+ catch (error) {
151
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
152
+ }
153
+ });
154
+ }
155
+ getAllToolDefinitions() {
156
+ return [
157
+ // Consolidated tools
158
+ {
159
+ name: "motion_projects",
160
+ description: "Manage Motion projects - supports create, list, and get operations",
161
+ inputSchema: {
162
+ type: "object",
163
+ properties: {
164
+ operation: {
165
+ type: "string",
166
+ enum: ["create", "list", "get"],
167
+ description: "Operation to perform"
168
+ },
169
+ projectId: {
170
+ type: "string",
171
+ description: "Project ID (required for get operation)"
172
+ },
173
+ workspaceId: {
174
+ type: "string",
175
+ description: "Workspace ID"
176
+ },
177
+ workspaceName: {
178
+ type: "string",
179
+ description: "Workspace name (alternative to ID)"
180
+ },
181
+ name: {
182
+ type: "string",
183
+ description: "Project name"
184
+ },
185
+ description: {
186
+ type: "string",
187
+ description: "Project description"
188
+ },
189
+ color: {
190
+ type: "string",
191
+ description: "Hex color code"
192
+ },
193
+ status: {
194
+ type: "string",
195
+ description: "Project status"
196
+ }
197
+ },
198
+ required: ["operation"]
199
+ }
200
+ },
201
+ {
202
+ name: "motion_tasks",
203
+ description: "Manage Motion tasks - supports create, list, get, update, delete, move, and unassign operations",
204
+ inputSchema: {
205
+ type: "object",
206
+ properties: {
207
+ operation: {
208
+ type: "string",
209
+ enum: ["create", "list", "get", "update", "delete", "move", "unassign"],
210
+ description: "Operation to perform"
211
+ },
212
+ taskId: {
213
+ type: "string",
214
+ description: "Task ID (required for get/update/delete/move/unassign)"
215
+ },
216
+ workspaceId: {
217
+ type: "string",
218
+ description: "Filter by workspace (for list)"
219
+ },
220
+ workspaceName: {
221
+ type: "string",
222
+ description: "Filter by workspace name (for list)"
223
+ },
224
+ projectId: {
225
+ type: "string",
226
+ description: "Filter by project (for list)"
227
+ },
228
+ projectName: {
229
+ type: "string",
230
+ description: "Project name (alternative to projectId)"
231
+ },
232
+ status: {
233
+ type: "string",
234
+ description: "Filter by status (for list)"
235
+ },
236
+ assigneeId: {
237
+ type: "string",
238
+ description: "Filter by assignee (for list)"
239
+ },
240
+ name: {
241
+ type: "string",
242
+ description: "Task name (required for create)"
243
+ },
244
+ description: {
245
+ type: "string",
246
+ description: "Task description"
247
+ },
248
+ priority: {
249
+ type: "string",
250
+ enum: ["ASAP", "HIGH", "MEDIUM", "LOW"],
251
+ description: "Task priority"
252
+ },
253
+ dueDate: {
254
+ type: "string",
255
+ description: "ISO 8601 format"
256
+ },
257
+ duration: {
258
+ oneOf: [
259
+ { type: "string", enum: ["NONE", "REMINDER"] },
260
+ { type: "number", minimum: 0 }
261
+ ],
262
+ description: "Minutes (as number) or 'NONE'/'REMINDER' (as string)"
263
+ },
264
+ labels: {
265
+ type: "array",
266
+ items: { type: "string" },
267
+ description: "Task labels"
268
+ },
269
+ autoScheduled: {
270
+ oneOf: [
271
+ { type: "object" },
272
+ { type: "null" }
273
+ ],
274
+ description: "Auto-scheduling configuration"
275
+ },
276
+ targetProjectId: {
277
+ type: "string",
278
+ description: "Target project for move operation"
279
+ },
280
+ targetWorkspaceId: {
281
+ type: "string",
282
+ description: "Target workspace for move operation"
283
+ },
284
+ limit: {
285
+ type: "number",
286
+ description: "Maximum number of tasks to return (for list)"
287
+ }
288
+ },
289
+ required: ["operation"]
290
+ }
291
+ },
292
+ // New consolidated tools for workspace and search management
293
+ {
294
+ name: "motion_workspaces",
295
+ description: "Manage Motion workspaces - supports list, get, and set_default operations",
296
+ inputSchema: {
297
+ type: "object",
298
+ properties: {
299
+ operation: {
300
+ type: "string",
301
+ enum: ["list", "get", "set_default"],
302
+ description: "Operation to perform"
303
+ },
304
+ workspaceId: {
305
+ type: "string",
306
+ description: "Workspace ID (required for get and set_default operations)"
307
+ }
308
+ },
309
+ required: ["operation"]
310
+ }
311
+ },
312
+ {
313
+ name: "motion_search",
314
+ description: "Search and context utilities for Motion - supports content, context, and smart operations",
315
+ inputSchema: {
316
+ type: "object",
317
+ properties: {
318
+ operation: {
319
+ type: "string",
320
+ enum: ["content", "context", "smart"],
321
+ description: "Operation to perform"
322
+ },
323
+ query: {
324
+ type: "string",
325
+ description: "Search query (required for content and smart operations)"
326
+ },
327
+ searchScope: {
328
+ type: "string",
329
+ enum: ["tasks", "projects", "both"],
330
+ description: "What to search for content operation (default: both)"
331
+ },
332
+ workspaceId: {
333
+ type: "string",
334
+ description: "Workspace ID to limit search/context"
335
+ },
336
+ workspaceName: {
337
+ type: "string",
338
+ description: "Workspace name (alternative to workspaceId)"
339
+ },
340
+ limit: {
341
+ type: "number",
342
+ description: "Maximum number of results for content operation"
343
+ },
344
+ includeProjects: {
345
+ type: "boolean",
346
+ description: "Include project information for context operation"
347
+ },
348
+ includeTasks: {
349
+ type: "boolean",
350
+ description: "Include task information for context operation"
351
+ },
352
+ includeUsers: {
353
+ type: "boolean",
354
+ description: "Include user information for context operation"
355
+ },
356
+ entityType: {
357
+ type: "string",
358
+ enum: ["project", "task"],
359
+ description: "Entity type for smart operation"
360
+ },
361
+ entityId: {
362
+ type: "string",
363
+ description: "Entity ID for smart operation"
364
+ },
365
+ includeRelated: {
366
+ type: "boolean",
367
+ description: "Include related entities for smart operation"
368
+ }
369
+ },
370
+ required: ["operation"]
371
+ }
372
+ },
373
+ {
374
+ name: "motion_users",
375
+ description: "Manage users and get current user information",
376
+ inputSchema: {
377
+ type: "object",
378
+ properties: {
379
+ operation: {
380
+ type: "string",
381
+ enum: ["list", "current"],
382
+ description: "Operation to perform"
383
+ },
384
+ workspaceId: {
385
+ type: "string",
386
+ description: "Workspace ID (optional for list operation, ignored for current)"
387
+ },
388
+ workspaceName: {
389
+ type: "string",
390
+ description: "Workspace name (alternative to workspaceId, ignored for current)"
391
+ }
392
+ },
393
+ required: ["operation"]
394
+ }
395
+ },
396
+ {
397
+ name: "motion_comments",
398
+ description: "Manage comments on tasks",
399
+ inputSchema: {
400
+ type: "object",
401
+ properties: {
402
+ operation: {
403
+ type: "string",
404
+ enum: ["list", "create"],
405
+ description: "Operation to perform"
406
+ },
407
+ taskId: {
408
+ type: "string",
409
+ description: "Task ID to comment on or fetch comments from (required)"
410
+ },
411
+ content: {
412
+ type: "string",
413
+ description: "Comment content (required for create operation)"
414
+ },
415
+ cursor: {
416
+ type: "string",
417
+ description: "Pagination cursor for list operation (optional)"
418
+ }
419
+ },
420
+ required: ["operation", "taskId"]
421
+ }
422
+ },
423
+ {
424
+ name: "motion_custom_fields",
425
+ description: "Manage custom fields for tasks and projects",
426
+ inputSchema: {
427
+ type: "object",
428
+ properties: {
429
+ operation: {
430
+ type: "string",
431
+ enum: ["list", "create", "delete", "add_to_project", "remove_from_project", "add_to_task", "remove_from_task"],
432
+ description: "Operation to perform"
433
+ },
434
+ fieldId: {
435
+ type: "string",
436
+ description: "Custom field ID"
437
+ },
438
+ workspaceId: {
439
+ type: "string",
440
+ description: "Workspace ID"
441
+ },
442
+ name: {
443
+ type: "string",
444
+ description: "Field name (for create)"
445
+ },
446
+ field: {
447
+ type: "string",
448
+ enum: ["text", "url", "date", "person", "multiPerson", "phone", "select", "multiSelect", "number", "email", "checkbox", "relatedTo"],
449
+ description: "Field type (for create)"
450
+ },
451
+ options: {
452
+ type: "array",
453
+ items: { type: "string" },
454
+ description: "Options for select/multiselect fields"
455
+ },
456
+ required: {
457
+ type: "boolean",
458
+ description: "Whether field is required"
459
+ },
460
+ projectId: {
461
+ type: "string",
462
+ description: "Project ID (for add/remove operations)"
463
+ },
464
+ taskId: {
465
+ type: "string",
466
+ description: "Task ID (for add/remove operations)"
467
+ },
468
+ value: {
469
+ type: ["string", "number", "boolean", "array", "null"],
470
+ description: "Field value"
471
+ }
472
+ },
473
+ required: ["operation", "workspaceId"]
474
+ }
475
+ },
476
+ {
477
+ name: "motion_recurring_tasks",
478
+ description: "Manage recurring tasks",
479
+ inputSchema: {
480
+ type: "object",
481
+ properties: {
482
+ operation: {
483
+ type: "string",
484
+ enum: ["list", "create", "delete"],
485
+ description: "Operation to perform"
486
+ },
487
+ recurringTaskId: {
488
+ type: "string",
489
+ description: "Recurring task ID (for delete)"
490
+ },
491
+ workspaceId: {
492
+ type: "string",
493
+ description: "Workspace ID"
494
+ },
495
+ name: {
496
+ type: "string",
497
+ description: "Task name (for create)"
498
+ },
499
+ description: {
500
+ type: "string",
501
+ description: "Task description"
502
+ },
503
+ projectId: {
504
+ type: "string",
505
+ description: "Project ID"
506
+ },
507
+ assigneeId: {
508
+ type: "string",
509
+ description: "User ID to assign the recurring task to (required for create)"
510
+ },
511
+ frequency: {
512
+ type: "object",
513
+ properties: {
514
+ type: {
515
+ type: "string",
516
+ enum: ["daily", "weekly", "monthly", "yearly"],
517
+ description: "Frequency type"
518
+ },
519
+ interval: {
520
+ type: "number",
521
+ description: "Repeat every N periods"
522
+ },
523
+ daysOfWeek: {
524
+ type: "array",
525
+ items: { type: "number" },
526
+ description: "0-6 for Sunday-Saturday (for weekly)"
527
+ },
528
+ dayOfMonth: {
529
+ type: "number",
530
+ description: "1-31 for monthly recurrence"
531
+ },
532
+ endDate: {
533
+ type: "string",
534
+ description: "ISO 8601 format end date"
535
+ }
536
+ },
537
+ required: ["type"],
538
+ description: "Frequency configuration (required for create)"
539
+ },
540
+ deadlineType: {
541
+ type: "string",
542
+ enum: ["HARD", "SOFT"],
543
+ description: "Deadline type (default: SOFT)"
544
+ },
545
+ duration: {
546
+ oneOf: [
547
+ { type: "number" },
548
+ { type: "string", enum: ["REMINDER"] }
549
+ ],
550
+ description: "Task duration in minutes or REMINDER"
551
+ },
552
+ startingOn: {
553
+ type: "string",
554
+ description: "Start date (ISO 8601 format)"
555
+ },
556
+ idealTime: {
557
+ type: "string",
558
+ description: "Ideal time in HH:mm format"
559
+ },
560
+ schedule: {
561
+ type: "string",
562
+ description: "Schedule name (default: Work Hours)"
563
+ },
564
+ priority: {
565
+ type: "string",
566
+ enum: ["ASAP", "HIGH", "MEDIUM", "LOW"],
567
+ description: "Task priority (default: MEDIUM)"
568
+ }
569
+ },
570
+ required: ["operation"]
571
+ }
572
+ },
573
+ {
574
+ name: "motion_schedules",
575
+ description: "Get user schedules showing their weekly working hours and time zones",
576
+ inputSchema: {
577
+ type: "object",
578
+ properties: {
579
+ userId: {
580
+ type: "string",
581
+ description: "User ID to get schedule for (optional, returns all schedules if not specified)"
582
+ },
583
+ startDate: {
584
+ type: "string",
585
+ description: "Start date for filtering schedules (optional)"
586
+ },
587
+ endDate: {
588
+ type: "string",
589
+ description: "End date for filtering schedules (optional)"
590
+ }
591
+ },
592
+ additionalProperties: false
593
+ }
594
+ },
595
+ {
596
+ name: "motion_statuses",
597
+ description: "Get available task/project statuses for a workspace",
598
+ inputSchema: {
599
+ type: "object",
600
+ properties: {
601
+ workspaceId: {
602
+ type: "string",
603
+ description: "Workspace ID to get statuses for (optional, returns all statuses if not specified)"
604
+ }
605
+ },
606
+ additionalProperties: false
607
+ }
608
+ }
609
+ ];
610
+ }
611
+ getEnabledTools() {
612
+ const allTools = this.getAllToolDefinitions();
613
+ const toolsMap = new Map(allTools.map(tool => [tool.name, tool]));
614
+ switch (this.toolsConfig) {
615
+ case 'minimal':
616
+ // Only core consolidated tools
617
+ return [
618
+ toolsMap.get('motion_tasks'),
619
+ toolsMap.get('motion_projects'),
620
+ toolsMap.get('motion_workspaces')
621
+ ].filter((tool) => tool !== undefined);
622
+ case 'essential':
623
+ // Consolidated tools plus commonly needed tools
624
+ return [
625
+ toolsMap.get('motion_tasks'),
626
+ toolsMap.get('motion_projects'),
627
+ toolsMap.get('motion_workspaces'),
628
+ toolsMap.get('motion_users'),
629
+ toolsMap.get('motion_search'),
630
+ toolsMap.get('motion_comments'),
631
+ toolsMap.get('motion_schedules')
632
+ ].filter((tool) => tool !== undefined);
633
+ case 'complete':
634
+ // All consolidated tools
635
+ return [
636
+ toolsMap.get('motion_tasks'),
637
+ toolsMap.get('motion_projects'),
638
+ toolsMap.get('motion_workspaces'),
639
+ toolsMap.get('motion_users'),
640
+ toolsMap.get('motion_search'),
641
+ toolsMap.get('motion_comments'),
642
+ toolsMap.get('motion_custom_fields'),
643
+ toolsMap.get('motion_recurring_tasks'),
644
+ toolsMap.get('motion_schedules'),
645
+ toolsMap.get('motion_statuses')
646
+ ].filter((tool) => tool !== undefined);
647
+ default:
648
+ // Handle custom configuration (already validated in validateToolsConfig)
649
+ if (this.toolsConfig.startsWith('custom:')) {
650
+ const customTools = this.toolsConfig.substring(7).split(',').map(s => s.trim());
651
+ return customTools
652
+ .map(name => toolsMap.get(name))
653
+ .filter((tool) => tool !== undefined);
654
+ }
655
+ // This should never happen since we validate in initialize()
656
+ // But if it does, throw an error instead of silently defaulting
657
+ throw new Error(`Unexpected tools configuration: ${this.toolsConfig}`);
658
+ }
659
+ }
660
+ // Handler methods
661
+ // Consolidated handlers for resource-based tools
662
+ async handleMotionProjects(args) {
663
+ const motionService = this.motionService;
664
+ const workspaceResolver = this.workspaceResolver;
665
+ if (!motionService || !workspaceResolver) {
666
+ return (0, utils_1.formatMcpError)(new Error("Services not available"));
667
+ }
668
+ const { operation, ...params } = args;
669
+ try {
670
+ switch (operation) {
671
+ case 'create':
672
+ if (!params.name) {
673
+ return (0, utils_1.formatMcpError)(new Error("Project name is required for create operation"));
674
+ }
675
+ const projectData = (0, utils_1.parseProjectArgs)(params);
676
+ const workspace = await workspaceResolver.resolveWorkspace({
677
+ workspaceId: projectData.workspaceId,
678
+ workspaceName: projectData.workspaceName
679
+ });
680
+ const project = await motionService.createProject({
681
+ ...projectData,
682
+ workspaceId: workspace.id
683
+ });
684
+ return (0, utils_1.formatMcpSuccess)(`Successfully created project "${project.name}" (ID: ${project.id})`);
685
+ case 'list':
686
+ const listWorkspace = await workspaceResolver.resolveWorkspace({
687
+ workspaceId: params.workspaceId,
688
+ workspaceName: params.workspaceName
689
+ });
690
+ const projects = await motionService.getProjects(listWorkspace.id);
691
+ return (0, utils_1.formatProjectList)(projects, listWorkspace.name);
692
+ case 'get':
693
+ if (!params.projectId) {
694
+ return (0, utils_1.formatMcpError)(new Error("Project ID is required for get operation"));
695
+ }
696
+ const projectDetails = await motionService.getProject(params.projectId);
697
+ return (0, utils_1.formatDetailResponse)(projectDetails, `Project details for "${projectDetails.name}"`);
698
+ default:
699
+ return (0, utils_1.formatMcpError)(new Error(`Unknown operation: ${operation}`));
700
+ }
701
+ }
702
+ catch (error) {
703
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
704
+ }
705
+ }
706
+ async handleMotionTasks(args) {
707
+ const motionService = this.motionService;
708
+ const workspaceResolver = this.workspaceResolver;
709
+ if (!motionService || !workspaceResolver) {
710
+ return (0, utils_1.formatMcpError)(new Error("Services not available"));
711
+ }
712
+ const { operation, ...params } = args;
713
+ try {
714
+ switch (operation) {
715
+ case 'create':
716
+ if (!params.name) {
717
+ return (0, utils_1.formatMcpError)(new Error("Task name is required for create operation"));
718
+ }
719
+ const taskData = (0, utils_1.parseTaskArgs)(params);
720
+ const workspace = await workspaceResolver.resolveWorkspace(taskData);
721
+ const task = await motionService.createTask({
722
+ ...taskData,
723
+ workspaceId: workspace.id
724
+ });
725
+ return (0, utils_1.formatMcpSuccess)(`Successfully created task "${task.name}" (ID: ${task.id})`);
726
+ case 'list':
727
+ const listWorkspace = await workspaceResolver.resolveWorkspace({
728
+ workspaceId: params.workspaceId,
729
+ workspaceName: params.workspaceName
730
+ });
731
+ const tasks = await motionService.getTasks(listWorkspace.id, params.projectId, 5, // maxPages
732
+ params.limit);
733
+ return (0, utils_1.formatTaskList)(tasks, {
734
+ workspaceName: listWorkspace.name,
735
+ projectName: params.projectName,
736
+ status: params.status,
737
+ limit: params.limit
738
+ });
739
+ case 'get':
740
+ if (!params.taskId) {
741
+ return (0, utils_1.formatMcpError)(new Error("Task ID is required for get operation"));
742
+ }
743
+ const taskDetails = await motionService.getTask(params.taskId);
744
+ return (0, utils_1.formatDetailResponse)(taskDetails, `Task details for "${taskDetails.name}"`);
745
+ case 'update':
746
+ if (!params.taskId) {
747
+ return (0, utils_1.formatMcpError)(new Error("Task ID is required for update operation"));
748
+ }
749
+ // Create update object with only valid MotionTask fields
750
+ const updateData = {};
751
+ if (params.name !== undefined)
752
+ updateData.name = params.name;
753
+ if (params.description !== undefined)
754
+ updateData.description = params.description;
755
+ if (params.status !== undefined)
756
+ updateData.status = params.status;
757
+ if (params.priority !== undefined)
758
+ updateData.priority = params.priority;
759
+ if (params.dueDate !== undefined)
760
+ updateData.dueDate = params.dueDate;
761
+ if (params.duration !== undefined) {
762
+ // Convert string duration to number or keep special values
763
+ if (typeof params.duration === 'string') {
764
+ if (params.duration === 'NONE' || params.duration === 'REMINDER') {
765
+ updateData.duration = params.duration;
766
+ }
767
+ else {
768
+ const numDuration = parseInt(params.duration, 10);
769
+ if (!isNaN(numDuration)) {
770
+ updateData.duration = numDuration;
771
+ }
772
+ }
773
+ }
774
+ else {
775
+ updateData.duration = params.duration;
776
+ }
777
+ }
778
+ if (params.labels !== undefined)
779
+ updateData.labels = params.labels?.map(name => ({ name }));
780
+ if (params.autoScheduled !== undefined)
781
+ updateData.autoScheduled = params.autoScheduled;
782
+ const updatedTask = await motionService.updateTask(params.taskId, updateData);
783
+ return (0, utils_1.formatMcpSuccess)(`Successfully updated task "${updatedTask.name}" (ID: ${updatedTask.id})`);
784
+ case 'delete':
785
+ if (!params.taskId) {
786
+ return (0, utils_1.formatMcpError)(new Error("Task ID is required for delete operation"));
787
+ }
788
+ await motionService.deleteTask(params.taskId);
789
+ return (0, utils_1.formatMcpSuccess)(`Successfully deleted task ${params.taskId}`);
790
+ case 'move':
791
+ if (!params.taskId) {
792
+ return (0, utils_1.formatMcpError)(new Error("Task ID is required for move operation"));
793
+ }
794
+ if (!params.targetProjectId && !params.targetWorkspaceId) {
795
+ return (0, utils_1.formatMcpError)(new Error("Either target project ID or target workspace ID is required for move operation"));
796
+ }
797
+ const movedTask = await motionService.moveTask(params.taskId, params.targetProjectId, params.targetWorkspaceId);
798
+ return (0, utils_1.formatMcpSuccess)(`Successfully moved task "${movedTask.name}" (ID: ${movedTask.id})`);
799
+ case 'unassign':
800
+ if (!params.taskId) {
801
+ return (0, utils_1.formatMcpError)(new Error("Task ID is required for unassign operation"));
802
+ }
803
+ const unassignedTask = await motionService.unassignTask(params.taskId);
804
+ return (0, utils_1.formatMcpSuccess)(`Successfully unassigned task "${unassignedTask.name}" (ID: ${unassignedTask.id})`);
805
+ default:
806
+ return (0, utils_1.formatMcpError)(new Error(`Unknown operation: ${operation}`));
807
+ }
808
+ }
809
+ catch (error) {
810
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
811
+ }
812
+ }
813
+ async handleSearchContent(args) {
814
+ const motionService = this.motionService;
815
+ const workspaceResolver = this.workspaceResolver;
816
+ if (!motionService || !workspaceResolver) {
817
+ return (0, utils_1.formatMcpError)(new Error("Services not available"));
818
+ }
819
+ const { query, entityTypes = ['projects', 'tasks'] } = args;
820
+ // Use configurable limit to prevent resource exhaustion
821
+ const limit = utils_1.LIMITS.MAX_SEARCH_RESULTS;
822
+ const workspace = await workspaceResolver.resolveWorkspace(args);
823
+ let results = [];
824
+ if (entityTypes?.includes('tasks')) {
825
+ // Pass limit to prevent fetching unlimited results
826
+ const tasks = await motionService.searchTasks(query, workspace.id, limit);
827
+ results.push(...tasks);
828
+ }
829
+ if (entityTypes?.includes('projects')) {
830
+ // Pass limit to prevent fetching unlimited results
831
+ const projects = await motionService.searchProjects(query, workspace.id, limit);
832
+ results.push(...projects);
833
+ }
834
+ return (0, utils_1.formatSearchResults)(results.slice(0, limit), query, { limit, searchScope: entityTypes?.join(',') || 'both' });
835
+ }
836
+ async handleGetContext(args) {
837
+ const { entityType, entityId, includeRelated = false } = args;
838
+ let contextText = `Context for ${entityType} ${entityId}:\n\n`;
839
+ // For now, return a simple context message as Motion API doesn't have specific context endpoints
840
+ if (entityType === 'project') {
841
+ contextText += `Project ID: ${entityId}\n`;
842
+ if (includeRelated) {
843
+ contextText += `Related tasks would be listed here (when available)\n`;
844
+ }
845
+ }
846
+ else if (entityType === 'task') {
847
+ contextText += `Task ID: ${entityId}\n`;
848
+ if (includeRelated) {
849
+ contextText += `Related project and subtasks would be listed here (when available)\n`;
850
+ }
851
+ }
852
+ return (0, utils_1.formatMcpSuccess)(contextText);
853
+ }
854
+ async handleMotionUsers(args) {
855
+ const motionService = this.motionService;
856
+ const workspaceResolver = this.workspaceResolver;
857
+ if (!motionService) {
858
+ return (0, utils_1.formatMcpError)(new Error("Motion service is not available"));
859
+ }
860
+ const { operation, workspaceId, workspaceName } = args;
861
+ try {
862
+ switch (operation) {
863
+ case 'list':
864
+ if (!workspaceResolver) {
865
+ return (0, utils_1.formatMcpError)(new Error("Workspace resolver not available"));
866
+ }
867
+ const workspace = await workspaceResolver.resolveWorkspace({ workspaceId, workspaceName });
868
+ const users = await motionService.getUsers(workspace.id);
869
+ const userList = users.map(u => `- ${u.name} (${u.email}) [ID: ${u.id}]`).join('\n');
870
+ return (0, utils_1.formatMcpSuccess)(`Users in workspace "${workspace.name}":\n${userList}`);
871
+ case 'current':
872
+ const currentUser = await motionService.getCurrentUser();
873
+ const userInfo = `Current user: ${currentUser.name || 'No name'} (${currentUser.email}) [ID: ${currentUser.id}]`;
874
+ return (0, utils_1.formatMcpSuccess)(userInfo);
875
+ default:
876
+ return (0, utils_1.formatMcpError)(new Error(`Unknown operation: ${operation}`));
877
+ }
878
+ }
879
+ catch (error) {
880
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
881
+ }
882
+ }
883
+ async handleMotionComments(args) {
884
+ const motionService = this.motionService;
885
+ if (!motionService) {
886
+ return (0, utils_1.formatMcpError)(new Error("Motion service is not available"));
887
+ }
888
+ const { operation, taskId, content, cursor } = args;
889
+ try {
890
+ switch (operation) {
891
+ case 'list':
892
+ if (!taskId) {
893
+ return (0, utils_1.formatMcpError)(new Error('taskId is required for list operation'));
894
+ }
895
+ const commentsResponse = await motionService.getComments(taskId, cursor);
896
+ // Format the comments list
897
+ const commentsResult = (0, utils_1.formatCommentList)(commentsResponse.data);
898
+ // Add pagination info if there's more data
899
+ if (commentsResponse.meta.nextCursor && commentsResult.content[0]) {
900
+ // Type guard to safely check if content[0] has text property
901
+ function hasTextProperty(obj) {
902
+ return typeof obj === 'object' && obj !== null && 'text' in obj && typeof obj.text === 'string';
903
+ }
904
+ if (hasTextProperty(commentsResult.content[0])) {
905
+ commentsResult.content[0].text += `\n\n📄 More comments available. Use cursor: ${commentsResponse.meta.nextCursor}`;
906
+ }
907
+ }
908
+ return commentsResult;
909
+ case 'create':
910
+ if (!taskId) {
911
+ return (0, utils_1.formatMcpError)(new Error('taskId is required for create operation'));
912
+ }
913
+ if (!content) {
914
+ return (0, utils_1.formatMcpError)(new Error('content is required for create operation'));
915
+ }
916
+ // Sanitize and validate comment content
917
+ const sanitizationResult = (0, sanitize_1.sanitizeCommentContent)(content);
918
+ if (!sanitizationResult.isValid) {
919
+ return (0, utils_1.formatMcpError)(new Error(sanitizationResult.error || 'Invalid comment content'));
920
+ }
921
+ const sanitizedContent = sanitizationResult.sanitized;
922
+ if (sanitizedContent.length > utils_1.LIMITS.COMMENT_MAX_LENGTH) {
923
+ return (0, utils_1.formatMcpError)(new Error(`Comment content exceeds maximum length of ${utils_1.LIMITS.COMMENT_MAX_LENGTH} characters`));
924
+ }
925
+ const commentData = {
926
+ taskId,
927
+ content: sanitizedContent
928
+ };
929
+ const newComment = await motionService.createComment(commentData);
930
+ return (0, utils_1.formatCommentDetail)(newComment);
931
+ default:
932
+ return (0, utils_1.formatMcpError)(new Error(`Unknown operation: ${operation}`));
933
+ }
934
+ }
935
+ catch (error) {
936
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
937
+ }
938
+ }
939
+ async handleMotionCustomFields(args) {
940
+ const motionService = this.motionService;
941
+ if (!motionService) {
942
+ return (0, utils_1.formatMcpError)(new Error("Motion service is not available"));
943
+ }
944
+ const { operation, fieldId, workspaceId, name, field, options, projectId, taskId, value } = args;
945
+ try {
946
+ switch (operation) {
947
+ case 'list':
948
+ if (!workspaceId) {
949
+ return (0, utils_1.formatMcpError)(new Error('Workspace ID is required for list operation'));
950
+ }
951
+ const fields = await motionService.getCustomFields(workspaceId);
952
+ return (0, utils_1.formatCustomFieldList)(fields);
953
+ case 'create':
954
+ if (!workspaceId) {
955
+ return (0, utils_1.formatMcpError)(new Error('Workspace ID is required for create operation'));
956
+ }
957
+ if (!name || !field) {
958
+ return (0, utils_1.formatMcpError)(new Error('Name and field are required for create operation'));
959
+ }
960
+ // Validate custom field name length
961
+ if (name.length > utils_1.LIMITS.CUSTOM_FIELD_NAME_MAX_LENGTH) {
962
+ return (0, utils_1.formatMcpError)(new Error(`Field name exceeds ${utils_1.LIMITS.CUSTOM_FIELD_NAME_MAX_LENGTH} characters`));
963
+ }
964
+ // Validate options parameter - only allowed for select/multiSelect fields
965
+ if (['select', 'multiSelect'].includes(field) !== Boolean(options)) {
966
+ return (0, utils_1.formatMcpError)(new Error('Options parameter only allowed for select/multiSelect field types'));
967
+ }
968
+ const fieldData = {
969
+ name,
970
+ field,
971
+ ...(options && { metadata: { options } })
972
+ };
973
+ const newField = await motionService.createCustomField(workspaceId, fieldData);
974
+ return (0, utils_1.formatCustomFieldDetail)(newField);
975
+ case 'delete':
976
+ if (!workspaceId) {
977
+ return (0, utils_1.formatMcpError)(new Error('Workspace ID is required for delete operation'));
978
+ }
979
+ if (!fieldId) {
980
+ return (0, utils_1.formatMcpError)(new Error('Field ID is required for delete operation'));
981
+ }
982
+ await motionService.deleteCustomField(workspaceId, fieldId);
983
+ return (0, utils_1.formatCustomFieldSuccess)('deleted');
984
+ case 'add_to_project':
985
+ if (!projectId || !fieldId) {
986
+ return (0, utils_1.formatMcpError)(new Error('Project ID and field ID are required for add_to_project operation'));
987
+ }
988
+ await motionService.addCustomFieldToProject(projectId, fieldId, value);
989
+ return (0, utils_1.formatCustomFieldSuccess)('added', 'project', projectId);
990
+ case 'remove_from_project':
991
+ if (!projectId || !fieldId) {
992
+ return (0, utils_1.formatMcpError)(new Error('Project ID and field ID are required for remove_from_project operation'));
993
+ }
994
+ await motionService.removeCustomFieldFromProject(projectId, fieldId);
995
+ return (0, utils_1.formatCustomFieldSuccess)('removed', 'project', projectId);
996
+ case 'add_to_task':
997
+ if (!taskId || !fieldId) {
998
+ return (0, utils_1.formatMcpError)(new Error('Task ID and field ID are required for add_to_task operation'));
999
+ }
1000
+ await motionService.addCustomFieldToTask(taskId, fieldId, value);
1001
+ return (0, utils_1.formatCustomFieldSuccess)('added', 'task', taskId);
1002
+ case 'remove_from_task':
1003
+ if (!taskId || !fieldId) {
1004
+ return (0, utils_1.formatMcpError)(new Error('Task ID and field ID are required for remove_from_task operation'));
1005
+ }
1006
+ await motionService.removeCustomFieldFromTask(taskId, fieldId);
1007
+ return (0, utils_1.formatCustomFieldSuccess)('removed', 'task', taskId);
1008
+ default:
1009
+ return (0, utils_1.formatMcpError)(new Error(`Unknown operation: ${operation}`));
1010
+ }
1011
+ }
1012
+ catch (error) {
1013
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
1014
+ }
1015
+ }
1016
+ async handleMotionRecurringTasks(args) {
1017
+ const motionService = this.motionService;
1018
+ const workspaceResolver = this.workspaceResolver;
1019
+ if (!motionService || !workspaceResolver) {
1020
+ return (0, utils_1.formatMcpError)(new Error("Services not available"));
1021
+ }
1022
+ const { operation, recurringTaskId, workspaceId, name, description, projectId, assigneeId, frequency, deadlineType, duration, startingOn, idealTime, schedule, priority } = args;
1023
+ try {
1024
+ switch (operation) {
1025
+ case 'list':
1026
+ if (!workspaceId) {
1027
+ return (0, utils_1.formatMcpError)(new Error('Workspace ID is required for list operation'));
1028
+ }
1029
+ const recurringTasks = await motionService.getRecurringTasks(workspaceId);
1030
+ return (0, utils_1.formatRecurringTaskList)(recurringTasks);
1031
+ case 'create':
1032
+ if (!name) {
1033
+ return (0, utils_1.formatMcpError)(new Error('Name is required for create operation'));
1034
+ }
1035
+ if (!workspaceId) {
1036
+ return (0, utils_1.formatMcpError)(new Error('Workspace ID is required for create operation'));
1037
+ }
1038
+ if (!assigneeId) {
1039
+ return (0, utils_1.formatMcpError)(new Error('Assignee ID is required for create operation'));
1040
+ }
1041
+ if (!frequency) {
1042
+ return (0, utils_1.formatMcpError)(new Error('Frequency is required for create operation'));
1043
+ }
1044
+ // Validate frequency
1045
+ if (!frequency.type || !['daily', 'weekly', 'monthly', 'yearly'].includes(frequency.type)) {
1046
+ return (0, utils_1.formatMcpError)(new Error('Frequency type must be one of: daily, weekly, monthly, yearly'));
1047
+ }
1048
+ // Validate frequency-specific fields
1049
+ if (frequency.interval && (!Number.isInteger(frequency.interval) || frequency.interval < 1)) {
1050
+ return (0, utils_1.formatMcpError)(new Error('Frequency interval must be a positive integer'));
1051
+ }
1052
+ if (frequency.daysOfWeek && !frequency.daysOfWeek.every(day => Number.isInteger(day) && day >= 0 && day <= 6)) {
1053
+ return (0, utils_1.formatMcpError)(new Error('daysOfWeek must contain integers between 0-6 (Sunday-Saturday)'));
1054
+ }
1055
+ if (frequency.dayOfMonth && (!Number.isInteger(frequency.dayOfMonth) || frequency.dayOfMonth < 1 || frequency.dayOfMonth > 31)) {
1056
+ return (0, utils_1.formatMcpError)(new Error('dayOfMonth must be an integer between 1-31'));
1057
+ }
1058
+ if (frequency.endDate) {
1059
+ const endDate = new Date(frequency.endDate);
1060
+ if (isNaN(endDate.getTime())) {
1061
+ return (0, utils_1.formatMcpError)(new Error('frequency.endDate must be a valid ISO 8601 date format'));
1062
+ }
1063
+ if (endDate <= new Date()) {
1064
+ return (0, utils_1.formatMcpError)(new Error('frequency.endDate must be in the future'));
1065
+ }
1066
+ }
1067
+ // Validate optional fields
1068
+ if (priority && !['ASAP', 'HIGH', 'MEDIUM', 'LOW'].includes(priority)) {
1069
+ return (0, utils_1.formatMcpError)(new Error('Priority must be one of: ASAP, HIGH, MEDIUM, LOW'));
1070
+ }
1071
+ if (deadlineType && !['HARD', 'SOFT'].includes(deadlineType)) {
1072
+ return (0, utils_1.formatMcpError)(new Error('Deadline type must be one of: HARD, SOFT'));
1073
+ }
1074
+ if (duration !== undefined) {
1075
+ if (typeof duration === 'number') {
1076
+ if (!Number.isInteger(duration) || duration <= 0) {
1077
+ return (0, utils_1.formatMcpError)(new Error('Duration must be a positive integer'));
1078
+ }
1079
+ }
1080
+ else if (typeof duration === 'string' && duration !== 'REMINDER') {
1081
+ return (0, utils_1.formatMcpError)(new Error('Duration string must be "REMINDER"'));
1082
+ }
1083
+ }
1084
+ if (startingOn) {
1085
+ const startDate = new Date(startingOn);
1086
+ if (isNaN(startDate.getTime())) {
1087
+ return (0, utils_1.formatMcpError)(new Error('startingOn must be a valid ISO 8601 date format'));
1088
+ }
1089
+ }
1090
+ if (idealTime && !/^\d{2}:\d{2}$/.test(idealTime)) {
1091
+ return (0, utils_1.formatMcpError)(new Error('idealTime must be in HH:mm format'));
1092
+ }
1093
+ const workspace = await workspaceResolver.resolveWorkspace({ workspaceId });
1094
+ const taskData = {
1095
+ name,
1096
+ workspaceId: workspace.id,
1097
+ assigneeId,
1098
+ frequency,
1099
+ ...(description && { description }),
1100
+ ...(projectId && { projectId }),
1101
+ ...(deadlineType && { deadlineType }),
1102
+ ...(duration && { duration }),
1103
+ ...(startingOn && { startingOn }),
1104
+ ...(idealTime && { idealTime }),
1105
+ ...(schedule && { schedule }),
1106
+ ...(priority && { priority })
1107
+ };
1108
+ const newTask = await motionService.createRecurringTask(taskData);
1109
+ return (0, utils_1.formatRecurringTaskDetail)(newTask);
1110
+ case 'delete':
1111
+ if (!recurringTaskId) {
1112
+ return (0, utils_1.formatMcpError)(new Error('Recurring task ID is required for delete operation'));
1113
+ }
1114
+ await motionService.deleteRecurringTask(recurringTaskId);
1115
+ return (0, utils_1.formatMcpSuccess)(`Recurring task ${recurringTaskId} deleted successfully`);
1116
+ default:
1117
+ return (0, utils_1.formatMcpError)(new Error(`Unknown operation: ${operation}`));
1118
+ }
1119
+ }
1120
+ catch (error) {
1121
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
1122
+ }
1123
+ }
1124
+ async handleMotionSchedules(args) {
1125
+ const motionService = this.motionService;
1126
+ if (!motionService) {
1127
+ return (0, utils_1.formatMcpError)(new Error("Motion service not available"));
1128
+ }
1129
+ const { userId, startDate, endDate } = args;
1130
+ // Validate date formats if provided using Date constructor
1131
+ const isValidDate = (dateStr) => {
1132
+ if (!dateStr)
1133
+ return true; // Optional fields are valid when not provided
1134
+ const date = new Date(dateStr);
1135
+ return !isNaN(date.getTime());
1136
+ };
1137
+ if (!isValidDate(startDate)) {
1138
+ return (0, utils_1.formatMcpError)(new Error('startDate must be a valid date string (e.g., "2024-01-15" or "2024-01-15T10:30:00Z")'));
1139
+ }
1140
+ if (!isValidDate(endDate)) {
1141
+ return (0, utils_1.formatMcpError)(new Error('endDate must be a valid date string (e.g., "2024-01-15" or "2024-01-15T10:30:00Z")'));
1142
+ }
1143
+ // Validate date range logic if both dates are provided
1144
+ if (startDate && endDate) {
1145
+ const start = new Date(startDate);
1146
+ const end = new Date(endDate);
1147
+ if (start > end) {
1148
+ return (0, utils_1.formatMcpError)(new Error('startDate must be before or equal to endDate'));
1149
+ }
1150
+ }
1151
+ try {
1152
+ const schedules = await motionService.getSchedules(userId, startDate, endDate);
1153
+ return (0, utils_1.formatScheduleList)(schedules);
1154
+ }
1155
+ catch (error) {
1156
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
1157
+ }
1158
+ }
1159
+ async handleMotionStatuses(args) {
1160
+ const motionService = this.motionService;
1161
+ if (!motionService) {
1162
+ return (0, utils_1.formatMcpError)(new Error("Motion service not available"));
1163
+ }
1164
+ const { workspaceId } = args;
1165
+ try {
1166
+ const statuses = await motionService.getStatuses(workspaceId);
1167
+ return (0, utils_1.formatStatusList)(statuses);
1168
+ }
1169
+ catch (error) {
1170
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
1171
+ }
1172
+ }
1173
+ // New consolidated handlers
1174
+ async handleMotionWorkspaces(args) {
1175
+ const motionService = this.motionService;
1176
+ if (!motionService) {
1177
+ return (0, utils_1.formatMcpError)(new Error("Motion service not available"));
1178
+ }
1179
+ const { operation, workspaceId } = args;
1180
+ try {
1181
+ switch (operation) {
1182
+ case 'list':
1183
+ const workspaces = await motionService.getWorkspaces();
1184
+ return (0, utils_1.formatWorkspaceList)(workspaces);
1185
+ case 'get':
1186
+ if (!workspaceId) {
1187
+ return (0, utils_1.formatMcpError)(new Error("Workspace ID is required for get operation"));
1188
+ }
1189
+ const workspaces_all = await motionService.getWorkspaces();
1190
+ const workspace = workspaces_all.find(w => w.id === workspaceId);
1191
+ if (!workspace) {
1192
+ return (0, utils_1.formatMcpError)(new Error(`Workspace not found: ${workspaceId}`));
1193
+ }
1194
+ return (0, utils_1.formatDetailResponse)(workspace, `Workspace details for "${workspace.name}"`);
1195
+ case 'set_default':
1196
+ if (!workspaceId) {
1197
+ return (0, utils_1.formatMcpError)(new Error("Workspace ID is required for set_default operation"));
1198
+ }
1199
+ // For now, we'll just return a success message since Motion API doesn't have a set default endpoint
1200
+ return (0, utils_1.formatMcpSuccess)(`Default workspace would be set to: ${workspaceId} (Note: This is a placeholder - actual implementation depends on Motion API support)`);
1201
+ default:
1202
+ return (0, utils_1.formatMcpError)(new Error(`Unknown operation: ${operation}`));
1203
+ }
1204
+ }
1205
+ catch (error) {
1206
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
1207
+ }
1208
+ }
1209
+ async handleMotionSearch(args) {
1210
+ const motionService = this.motionService;
1211
+ const workspaceResolver = this.workspaceResolver;
1212
+ if (!motionService || !workspaceResolver) {
1213
+ return (0, utils_1.formatMcpError)(new Error("Services not available"));
1214
+ }
1215
+ const { operation } = args;
1216
+ try {
1217
+ switch (operation) {
1218
+ case 'content':
1219
+ if (!args.query) {
1220
+ return (0, utils_1.formatMcpError)(new Error("Query is required for content search"));
1221
+ }
1222
+ return await this.handleSearchContent({
1223
+ query: args.query,
1224
+ entityTypes: args.searchScope === 'tasks' ? ['tasks'] :
1225
+ args.searchScope === 'projects' ? ['projects'] :
1226
+ ['tasks', 'projects'],
1227
+ workspaceId: args.workspaceId,
1228
+ workspaceName: args.workspaceName
1229
+ });
1230
+ case 'context':
1231
+ if (!args.entityType || !args.entityId) {
1232
+ return (0, utils_1.formatMcpError)(new Error("EntityType and entityId are required for context operation"));
1233
+ }
1234
+ return await this.handleGetContext({
1235
+ entityType: args.entityType,
1236
+ entityId: args.entityId,
1237
+ includeRelated: args.includeRelated
1238
+ });
1239
+ case 'smart':
1240
+ // Smart search combines content search with contextual information
1241
+ const { query, entityType, entityId, includeRelated = false } = args;
1242
+ if (!query) {
1243
+ return (0, utils_1.formatMcpError)(new Error("Query is required for smart search"));
1244
+ }
1245
+ let contextText = `Smart search results for "${query}":\n\n`;
1246
+ // Perform content search
1247
+ const searchResults = await this.handleSearchContent({
1248
+ query,
1249
+ entityTypes: args.searchScope === 'tasks' ? ['tasks'] :
1250
+ args.searchScope === 'projects' ? ['projects'] :
1251
+ ['tasks', 'projects'],
1252
+ workspaceId: args.workspaceId,
1253
+ workspaceName: args.workspaceName
1254
+ });
1255
+ contextText += "Content Results:\n";
1256
+ // Extract text from search results
1257
+ if ('content' in searchResults && searchResults.content && Array.isArray(searchResults.content)) {
1258
+ const textContent = searchResults.content.find(item => item.type === 'text');
1259
+ if (textContent && 'text' in textContent) {
1260
+ contextText += textContent.text + "\n\n";
1261
+ }
1262
+ }
1263
+ // Add contextual information if entity is specified
1264
+ if (entityType && entityId) {
1265
+ contextText += `Context for ${entityType} ${entityId}:\n`;
1266
+ if (entityType === 'project') {
1267
+ contextText += `Project ID: ${entityId}\n`;
1268
+ if (includeRelated) {
1269
+ contextText += `Related tasks would be listed here (when available)\n`;
1270
+ }
1271
+ }
1272
+ else if (entityType === 'task') {
1273
+ contextText += `Task ID: ${entityId}\n`;
1274
+ if (includeRelated) {
1275
+ contextText += `Related project and subtasks would be listed here (when available)\n`;
1276
+ }
1277
+ }
1278
+ }
1279
+ return (0, utils_1.formatMcpSuccess)(contextText);
1280
+ default:
1281
+ return (0, utils_1.formatMcpError)(new Error(`Unknown operation: ${operation}`));
1282
+ }
1283
+ }
1284
+ catch (error) {
1285
+ return (0, utils_1.formatMcpError)(error instanceof Error ? error : new Error(String(error)));
1286
+ }
1287
+ }
1288
+ async run() {
1289
+ await this.initialize();
1290
+ const transport = new stdio_js_1.StdioServerTransport();
1291
+ await this.server.connect(transport);
1292
+ (0, utils_1.mcpLog)(utils_1.LOG_LEVELS.INFO, "Motion MCP Server running on stdio");
1293
+ }
1294
+ }
1295
+ // Main execution
1296
+ if (require.main === module) {
1297
+ const server = new MotionMCPServer();
1298
+ server.run().catch((error) => {
1299
+ console.error("Failed to run server:", error);
1300
+ process.exit(1);
1301
+ });
1302
+ }
1303
+ exports.default = MotionMCPServer;
1304
+ //# sourceMappingURL=mcp-server-old.js.map