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.
@@ -0,0 +1,1137 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
4
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ const {
6
+ ListToolsRequestSchema,
7
+ CallToolRequestSchema,
8
+ } = require("@modelcontextprotocol/sdk/types.js");
9
+ const MotionApiService = require('./services/motionApi.js');
10
+ require('dotenv').config();
11
+
12
+ class MotionMCPServer {
13
+ constructor() {
14
+ this.server = new Server(
15
+ {
16
+ name: "motion-mcp-server",
17
+ version: "1.0.0",
18
+ },
19
+ {
20
+ capabilities: {
21
+ tools: {},
22
+ },
23
+ }
24
+ );
25
+
26
+ this.motionService = null;
27
+ this.setupHandlers();
28
+ }
29
+
30
+ async initialize() {
31
+ try {
32
+ this.motionService = new MotionApiService();
33
+ } catch (error) {
34
+ console.error("Failed to initialize Motion API service:", error.message);
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ setupHandlers() {
40
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
41
+ return {
42
+ tools: [
43
+ {
44
+ name: "create_motion_project",
45
+ description: "Create a new project in Motion",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {
49
+ name: {
50
+ type: "string",
51
+ description: "Project name (required)"
52
+ },
53
+ description: {
54
+ type: "string",
55
+ description: "Project description (optional)"
56
+ },
57
+ color: {
58
+ type: "string",
59
+ description: "Project color in hex format (optional, e.g., #FF5733)"
60
+ },
61
+ status: {
62
+ type: "string",
63
+ description: "Project status (optional)"
64
+ }
65
+ },
66
+ required: ["name"]
67
+ }
68
+ },
69
+ {
70
+ name: "list_motion_projects",
71
+ description: "List all projects in Motion. If no workspace is specified, will use the default workspace. You can ask the user which workspace they prefer.",
72
+ inputSchema: {
73
+ type: "object",
74
+ properties: {
75
+ workspaceId: {
76
+ type: "string",
77
+ description: "Optional workspace ID to filter projects. If not provided, uses default workspace."
78
+ },
79
+ workspaceName: {
80
+ type: "string",
81
+ description: "Optional workspace name to filter projects (alternative to workspaceId)."
82
+ }
83
+ },
84
+ additionalProperties: false
85
+ }
86
+ },
87
+ {
88
+ name: "get_motion_project",
89
+ description: "Get a specific project by ID from Motion",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ projectId: {
94
+ type: "string",
95
+ description: "The project ID to retrieve"
96
+ }
97
+ },
98
+ required: ["projectId"]
99
+ }
100
+ },
101
+ {
102
+ name: "update_motion_project",
103
+ description: "Update an existing project in Motion",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ projectId: {
108
+ type: "string",
109
+ description: "The project ID to update"
110
+ },
111
+ name: {
112
+ type: "string",
113
+ description: "Updated project name (optional)"
114
+ },
115
+ description: {
116
+ type: "string",
117
+ description: "Updated project description (optional)"
118
+ },
119
+ color: {
120
+ type: "string",
121
+ description: "Updated project color in hex format (optional)"
122
+ },
123
+ status: {
124
+ type: "string",
125
+ description: "Updated project status (optional)"
126
+ }
127
+ },
128
+ required: ["projectId"]
129
+ }
130
+ },
131
+ {
132
+ name: "delete_motion_project",
133
+ description: "Delete a project from Motion",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ projectId: {
138
+ type: "string",
139
+ description: "The project ID to delete"
140
+ }
141
+ },
142
+ required: ["projectId"]
143
+ }
144
+ },
145
+ {
146
+ name: "create_motion_task",
147
+ description: "Create a new task in Motion. A workspaceId is required - if not provided, will use the default workspace. If projectId is not specified but a project name is mentioned, will try to find the project in the workspace.",
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ name: {
152
+ type: "string",
153
+ description: "Task name (required)"
154
+ },
155
+ description: {
156
+ type: "string",
157
+ description: "Task description (optional, supports Markdown)"
158
+ },
159
+ workspaceId: {
160
+ type: "string",
161
+ description: "Workspace ID where the task should be created. If not provided, will use the default workspace."
162
+ },
163
+ workspaceName: {
164
+ type: "string",
165
+ description: "Workspace name (alternative to workspaceId). Will be resolved to workspaceId."
166
+ },
167
+ projectId: {
168
+ type: "string",
169
+ description: "Project ID to assign task to (optional). If not provided, task will be created without a project."
170
+ },
171
+ projectName: {
172
+ type: "string",
173
+ description: "Project name (alternative to projectId). Will be resolved to projectId within the specified workspace."
174
+ },
175
+ status: {
176
+ type: "string",
177
+ description: "Task status (optional). If not provided, uses workspace default status."
178
+ },
179
+ priority: {
180
+ type: "string",
181
+ description: "Task priority: ASAP, HIGH, MEDIUM, or LOW (optional, defaults to MEDIUM)"
182
+ },
183
+ dueDate: {
184
+ type: "string",
185
+ description: "Task due date in ISO 8601 format (optional, required for scheduled tasks)"
186
+ },
187
+ duration: {
188
+ type: ["string", "number"],
189
+ description: "Task duration in minutes (number) or 'NONE' or 'REMINDER' (optional)"
190
+ },
191
+ assigneeId: {
192
+ type: "string",
193
+ description: "User ID to assign task to (optional)"
194
+ },
195
+ labels: {
196
+ type: "array",
197
+ items: { type: "string" },
198
+ description: "Array of label names to add to the task (optional)"
199
+ },
200
+ autoScheduled: {
201
+ type: ["object", "null"],
202
+ description: "Auto-scheduling settings (optional). Set to null to disable auto-scheduling."
203
+ }
204
+ },
205
+ required: ["name"]
206
+ }
207
+ },
208
+ {
209
+ name: "list_motion_tasks",
210
+ description: "List tasks in Motion with optional filters. If no workspace is specified, will use the default workspace.",
211
+ inputSchema: {
212
+ type: "object",
213
+ properties: {
214
+ workspaceId: {
215
+ type: "string",
216
+ description: "Optional workspace ID to filter tasks. If not provided, uses default workspace."
217
+ },
218
+ workspaceName: {
219
+ type: "string",
220
+ description: "Optional workspace name to filter tasks (alternative to workspaceId)."
221
+ },
222
+ projectId: {
223
+ type: "string",
224
+ description: "Filter tasks by project ID (optional)"
225
+ },
226
+ status: {
227
+ type: "string",
228
+ description: "Filter tasks by status (optional)"
229
+ },
230
+ assigneeId: {
231
+ type: "string",
232
+ description: "Filter tasks by assignee ID (optional)"
233
+ }
234
+ },
235
+ additionalProperties: false
236
+ }
237
+ },
238
+ {
239
+ name: "get_motion_task",
240
+ description: "Get a specific task by ID from Motion",
241
+ inputSchema: {
242
+ type: "object",
243
+ properties: {
244
+ taskId: {
245
+ type: "string",
246
+ description: "The task ID to retrieve"
247
+ }
248
+ },
249
+ required: ["taskId"]
250
+ }
251
+ },
252
+ {
253
+ name: "update_motion_task",
254
+ description: "Update an existing task in Motion",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ taskId: {
259
+ type: "string",
260
+ description: "The task ID to update"
261
+ },
262
+ name: {
263
+ type: "string",
264
+ description: "Updated task name (optional)"
265
+ },
266
+ description: {
267
+ type: "string",
268
+ description: "Updated task description (optional)"
269
+ },
270
+ status: {
271
+ type: "string",
272
+ description: "Updated task status (optional)"
273
+ },
274
+ priority: {
275
+ type: "string",
276
+ description: "Updated task priority (optional)"
277
+ },
278
+ dueDate: {
279
+ type: "string",
280
+ description: "Updated task due date in ISO format (optional)"
281
+ },
282
+ projectId: {
283
+ type: "string",
284
+ description: "Updated project ID (optional)"
285
+ },
286
+ assigneeId: {
287
+ type: "string",
288
+ description: "Updated assignee ID (optional)"
289
+ }
290
+ },
291
+ required: ["taskId"]
292
+ }
293
+ },
294
+ {
295
+ name: "delete_motion_task",
296
+ description: "Delete a task from Motion",
297
+ inputSchema: {
298
+ type: "object",
299
+ properties: {
300
+ taskId: {
301
+ type: "string",
302
+ description: "The task ID to delete"
303
+ }
304
+ },
305
+ required: ["taskId"]
306
+ }
307
+ },
308
+ {
309
+ name: "list_motion_workspaces",
310
+ description: "List all workspaces in Motion. Use this to show users available workspaces so they can choose which one to work with.",
311
+ inputSchema: {
312
+ type: "object",
313
+ properties: {},
314
+ additionalProperties: false
315
+ }
316
+ },
317
+ {
318
+ name: "list_motion_users",
319
+ description: "List all users in Motion",
320
+ inputSchema: {
321
+ type: "object",
322
+ properties: {},
323
+ additionalProperties: false
324
+ }
325
+ },
326
+ {
327
+ name: "get_motion_context",
328
+ description: "Get current Motion context and intelligent defaults. This tool provides the LLM with comprehensive context about the user's Motion workspace, including default workspace, recent activity, and smart suggestions. Use this tool first to understand the user's current state.",
329
+ inputSchema: {
330
+ type: "object",
331
+ properties: {
332
+ includeRecentActivity: {
333
+ type: "boolean",
334
+ description: "Include recent tasks and projects (optional, defaults to true)"
335
+ },
336
+ includeWorkloadSummary: {
337
+ type: "boolean",
338
+ description: "Include workload and task distribution summary (optional, defaults to true)"
339
+ },
340
+ includeSuggestions: {
341
+ type: "boolean",
342
+ description: "Include intelligent suggestions for next actions (optional, defaults to true)"
343
+ }
344
+ },
345
+ additionalProperties: false
346
+ }
347
+ },
348
+ {
349
+ name: "search_motion_content",
350
+ description: "Perform intelligent search across tasks and projects by content, keywords, or semantic meaning. This goes beyond simple name matching to search task descriptions, project details, and related content.",
351
+ inputSchema: {
352
+ type: "object",
353
+ properties: {
354
+ query: {
355
+ type: "string",
356
+ description: "Search query - can be keywords, phrases, or semantic descriptions"
357
+ },
358
+ searchScope: {
359
+ type: "string",
360
+ enum: ["tasks", "projects", "both"],
361
+ description: "What to search (optional, defaults to 'both')"
362
+ },
363
+ workspaceId: {
364
+ type: "string",
365
+ description: "Limit search to specific workspace (optional, defaults to all accessible workspaces)"
366
+ },
367
+ limit: {
368
+ type: "number",
369
+ description: "Maximum number of results to return (optional, defaults to 20)"
370
+ }
371
+ },
372
+ required: ["query"]
373
+ }
374
+ },
375
+ {
376
+ name: "analyze_motion_workload",
377
+ description: "Analyze current workload, overdue tasks, upcoming deadlines, and provide insights about task distribution and priorities. Helpful for understanding user's current situation and providing intelligent suggestions.",
378
+ inputSchema: {
379
+ type: "object",
380
+ properties: {
381
+ workspaceId: {
382
+ type: "string",
383
+ description: "Workspace to analyze (optional, defaults to all workspaces)"
384
+ },
385
+ timeframe: {
386
+ type: "string",
387
+ enum: ["today", "this_week", "this_month", "next_week"],
388
+ description: "Time period to analyze (optional, defaults to 'this_week')"
389
+ },
390
+ includeProjects: {
391
+ type: "boolean",
392
+ description: "Include project-level analysis (optional, defaults to true)"
393
+ }
394
+ },
395
+ additionalProperties: false
396
+ }
397
+ },
398
+ {
399
+ name: "suggest_next_actions",
400
+ description: "Provide intelligent suggestions for next actions based on current workload, priorities, deadlines, and project status. Helps LLM provide proactive assistance.",
401
+ inputSchema: {
402
+ type: "object",
403
+ properties: {
404
+ workspaceId: {
405
+ type: "string",
406
+ description: "Workspace to analyze for suggestions (optional, uses default workspace)"
407
+ },
408
+ context: {
409
+ type: "string",
410
+ description: "Current context or goal (optional, e.g., 'daily planning', 'project review', 'end of week')"
411
+ },
412
+ maxSuggestions: {
413
+ type: "number",
414
+ description: "Maximum number of suggestions to return (optional, defaults to 5)"
415
+ }
416
+ },
417
+ additionalProperties: false
418
+ }
419
+ },
420
+ {
421
+ name: "create_project_template",
422
+ description: "Create a new project with a predefined template including common tasks, structure, and best practices. Templates can be customized based on project type and user preferences.",
423
+ inputSchema: {
424
+ type: "object",
425
+ properties: {
426
+ name: {
427
+ type: "string",
428
+ description: "Project name"
429
+ },
430
+ templateType: {
431
+ type: "string",
432
+ enum: ["software_development", "marketing_campaign", "research_project", "event_planning", "content_creation", "general"],
433
+ description: "Type of project template to use"
434
+ },
435
+ workspaceId: {
436
+ type: "string",
437
+ description: "Workspace where project should be created (optional, uses default)"
438
+ },
439
+ customizations: {
440
+ type: "object",
441
+ description: "Template customizations (optional)",
442
+ properties: {
443
+ includeTaskTemplates: {
444
+ type: "boolean",
445
+ description: "Whether to create template tasks (defaults to true)"
446
+ },
447
+ projectDuration: {
448
+ type: "string",
449
+ description: "Expected project duration (e.g., '2 weeks', '3 months')"
450
+ },
451
+ teamSize: {
452
+ type: "number",
453
+ description: "Expected team size for task assignment"
454
+ }
455
+ }
456
+ }
457
+ },
458
+ required: ["name", "templateType"]
459
+ }
460
+ },
461
+ {
462
+ name: "bulk_update_tasks",
463
+ description: "Update multiple tasks at once with the same changes. Useful for batch operations like changing status, priority, or assignee for multiple tasks.",
464
+ inputSchema: {
465
+ type: "object",
466
+ properties: {
467
+ taskIds: {
468
+ type: "array",
469
+ items: { type: "string" },
470
+ description: "Array of task IDs to update"
471
+ },
472
+ updates: {
473
+ type: "object",
474
+ description: "Updates to apply to all tasks",
475
+ properties: {
476
+ status: { type: "string" },
477
+ priority: { type: "string" },
478
+ assigneeId: { type: "string" },
479
+ projectId: { type: "string" },
480
+ dueDate: { type: "string" }
481
+ }
482
+ },
483
+ workspaceId: {
484
+ type: "string",
485
+ description: "Workspace ID for validation (optional, uses default)"
486
+ }
487
+ },
488
+ required: ["taskIds", "updates"]
489
+ }
490
+ },
491
+ {
492
+ name: "smart_schedule_tasks",
493
+ description: "Intelligently schedule multiple tasks based on priorities, deadlines, estimated durations, and availability. Provides optimal scheduling suggestions.",
494
+ inputSchema: {
495
+ type: "object",
496
+ properties: {
497
+ taskIds: {
498
+ type: "array",
499
+ items: { type: "string" },
500
+ description: "Array of task IDs to schedule (optional, will auto-select unscheduled tasks if not provided)"
501
+ },
502
+ workspaceId: {
503
+ type: "string",
504
+ description: "Workspace to schedule tasks in (optional, uses default)"
505
+ },
506
+ schedulingPreferences: {
507
+ type: "object",
508
+ description: "Scheduling preferences (optional)",
509
+ properties: {
510
+ prioritizeDeadlines: {
511
+ type: "boolean",
512
+ description: "Prioritize tasks with deadlines (defaults to true)"
513
+ },
514
+ respectPriorities: {
515
+ type: "boolean",
516
+ description: "Schedule higher priority tasks first (defaults to true)"
517
+ },
518
+ includeBufferTime: {
519
+ type: "boolean",
520
+ description: "Add buffer time between tasks (defaults to true)"
521
+ }
522
+ }
523
+ }
524
+ },
525
+ additionalProperties: false
526
+ }
527
+ },
528
+ ]
529
+ };
530
+ });
531
+
532
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
533
+ const { name, arguments: args } = request.params;
534
+
535
+ try {
536
+ switch (name) {
537
+ case "create_motion_project":
538
+ return await this.handleCreateProject(args);
539
+
540
+ case "list_motion_projects":
541
+ return await this.handleListProjects(args);
542
+
543
+ case "get_motion_project":
544
+ return await this.handleGetProject(args);
545
+
546
+ case "update_motion_project":
547
+ return await this.handleUpdateProject(args);
548
+
549
+ case "delete_motion_project":
550
+ return await this.handleDeleteProject(args);
551
+
552
+ case "create_motion_task":
553
+ return await this.handleCreateTask(args);
554
+
555
+ case "list_motion_tasks":
556
+ return await this.handleListTasks(args);
557
+
558
+ case "get_motion_task":
559
+ return await this.handleGetTask(args);
560
+
561
+ case "update_motion_task":
562
+ return await this.handleUpdateTask(args);
563
+
564
+ case "delete_motion_task":
565
+ return await this.handleDeleteTask(args);
566
+
567
+ case "list_motion_workspaces":
568
+ return await this.handleListWorkspaces();
569
+
570
+ case "list_motion_users":
571
+ return await this.handleListUsers();
572
+
573
+ case "get_motion_context":
574
+ return await this.handleGetContext(args);
575
+
576
+ case "search_motion_content":
577
+ return await this.handleSearchContent(args);
578
+
579
+ case "analyze_motion_workload":
580
+ return await this.handleAnalyzeWorkload(args);
581
+
582
+ case "suggest_next_actions":
583
+ return await this.handleSuggestNextActions(args);
584
+
585
+ case "create_project_template":
586
+ return await this.handleCreateProjectTemplate(args);
587
+
588
+ case "bulk_update_tasks":
589
+ return await this.handleBulkUpdateTasks(args);
590
+
591
+ case "smart_schedule_tasks":
592
+ return await this.handleSmartScheduleTasks(args);
593
+
594
+ default:
595
+ throw new Error(`Unknown tool: ${name}`);
596
+ }
597
+ } catch (error) {
598
+ return {
599
+ content: [
600
+ {
601
+ type: "text",
602
+ text: `Error: ${error.message}`
603
+ }
604
+ ],
605
+ isError: true
606
+ };
607
+ }
608
+ });
609
+ }
610
+
611
+ async handleCreateProject(args) {
612
+ const project = await this.motionService.createProject(args);
613
+ return {
614
+ content: [
615
+ {
616
+ type: "text",
617
+ text: `Successfully created project "${project.name}" with ID: ${project.id}`
618
+ }
619
+ ]
620
+ };
621
+ }
622
+
623
+ async handleListProjects(args = {}) {
624
+ let workspaceId = args.workspaceId;
625
+ let workspaceName = null;
626
+
627
+ // If workspace name provided instead of ID, look it up
628
+ if (!workspaceId && args.workspaceName) {
629
+ try {
630
+ const workspace = await this.motionService.getWorkspaceByName(args.workspaceName);
631
+ workspaceId = workspace.id;
632
+ workspaceName = workspace.name;
633
+ } catch (error) {
634
+ return {
635
+ content: [
636
+ {
637
+ type: "text",
638
+ text: `Error: Could not find workspace "${args.workspaceName}". Available workspaces can be listed with list_motion_workspaces.`
639
+ }
640
+ ],
641
+ isError: true
642
+ };
643
+ }
644
+ }
645
+
646
+ // Get projects for the specified or default workspace
647
+ const projects = await this.motionService.getProjects(workspaceId);
648
+
649
+ // Get workspace info for context
650
+ if (!workspaceName && workspaceId) {
651
+ try {
652
+ const workspaces = await this.motionService.getWorkspaces();
653
+ const workspace = workspaces.find(w => w.id === workspaceId);
654
+ workspaceName = workspace ? workspace.name : 'Unknown';
655
+ } catch (error) {
656
+ workspaceName = 'Unknown';
657
+ }
658
+ } else if (!workspaceName) {
659
+ try {
660
+ const defaultWorkspace = await this.motionService.getDefaultWorkspace();
661
+ workspaceName = defaultWorkspace.name;
662
+ workspaceId = defaultWorkspace.id;
663
+ } catch (error) {
664
+ workspaceName = 'Default';
665
+ }
666
+ }
667
+
668
+ const projectList = projects.map(p => `- ${p.name} (ID: ${p.id})`).join('\n');
669
+
670
+ let responseText = `Found ${projects.length} projects in workspace "${workspaceName}"`;
671
+ if (workspaceId) {
672
+ responseText += ` (ID: ${workspaceId})`;
673
+ }
674
+ responseText += `:\n${projectList}`;
675
+
676
+ // If no workspace was specified and there are multiple workspaces, suggest the user can specify one
677
+ if (!args.workspaceId && !args.workspaceName) {
678
+ responseText += `\n\nNote: This shows projects from the default workspace. You can specify a different workspace using the workspaceId or workspaceName parameter, or use list_motion_workspaces to see all available workspaces.`;
679
+ }
680
+
681
+ return {
682
+ content: [
683
+ {
684
+ type: "text",
685
+ text: responseText
686
+ }
687
+ ]
688
+ };
689
+ }
690
+
691
+ async handleGetProject(args) {
692
+ const project = await this.motionService.getProject(args.projectId);
693
+ return {
694
+ content: [
695
+ {
696
+ type: "text",
697
+ text: `Project Details:\n- Name: ${project.name}\n- ID: ${project.id}\n- Description: ${project.description || 'N/A'}\n- Status: ${project.status || 'N/A'}`
698
+ }
699
+ ]
700
+ };
701
+ }
702
+
703
+ async handleUpdateProject(args) {
704
+ const { projectId, ...updateData } = args;
705
+ const project = await this.motionService.updateProject(projectId, updateData);
706
+ return {
707
+ content: [
708
+ {
709
+ type: "text",
710
+ text: `Successfully updated project "${project.name}" (ID: ${project.id})`
711
+ }
712
+ ]
713
+ };
714
+ }
715
+
716
+ async handleDeleteProject(args) {
717
+ await this.motionService.deleteProject(args.projectId);
718
+ return {
719
+ content: [
720
+ {
721
+ type: "text",
722
+ text: `Successfully deleted project with ID: ${args.projectId}`
723
+ }
724
+ ]
725
+ };
726
+ }
727
+
728
+ async handleCreateTask(args) {
729
+ try {
730
+ // Resolve workspace ID if needed
731
+ let workspaceId = args.workspaceId;
732
+ if (!workspaceId && args.workspaceName) {
733
+ const workspace = await this.motionService.getWorkspaceByName(args.workspaceName);
734
+ workspaceId = workspace.id;
735
+ } else if (!workspaceId) {
736
+ const defaultWorkspace = await this.motionService.getDefaultWorkspace();
737
+ workspaceId = defaultWorkspace.id;
738
+ }
739
+
740
+ // Resolve project ID if needed
741
+ let projectId = args.projectId;
742
+ if (!projectId && args.projectName) {
743
+ try {
744
+ const project = await this.motionService.getProjectByName(args.projectName, workspaceId);
745
+ projectId = project.id;
746
+ } catch (projectError) {
747
+ throw new Error(`Project "${args.projectName}" not found in workspace`);
748
+ }
749
+ }
750
+
751
+ // Build task data with required workspaceId
752
+ const taskData = {
753
+ name: args.name,
754
+ workspaceId, // Required by Motion API
755
+ ...(args.description && { description: args.description }),
756
+ ...(projectId && { projectId }),
757
+ ...(args.status && { status: args.status }),
758
+ ...(args.priority && { priority: args.priority }),
759
+ ...(args.dueDate && { dueDate: args.dueDate }),
760
+ ...(args.duration && { duration: args.duration }),
761
+ ...(args.assigneeId && { assigneeId: args.assigneeId }),
762
+ ...(args.labels && { labels: args.labels }),
763
+ ...(args.autoScheduled !== undefined && { autoScheduled: args.autoScheduled })
764
+ };
765
+
766
+ const task = await this.motionService.createTask(taskData);
767
+
768
+ return {
769
+ content: [
770
+ {
771
+ type: "text",
772
+ text: `Successfully created task "${task.name}" with ID: ${task.id}${projectId ? ` in project ${projectId}` : ''} in workspace ${workspaceId}`
773
+ }
774
+ ]
775
+ };
776
+ } catch (error) {
777
+ return {
778
+ content: [
779
+ {
780
+ type: "text",
781
+ text: `Failed to create task: ${error.message}`
782
+ }
783
+ ],
784
+ isError: true
785
+ };
786
+ }
787
+ }
788
+
789
+ async handleListTasks(args = {}) {
790
+ const tasks = await this.motionService.getTasks(args);
791
+ const taskList = tasks.map(t => `- ${t.name} (ID: ${t.id}) - Status: ${t.status || 'N/A'}`).join('\n');
792
+ return {
793
+ content: [
794
+ {
795
+ type: "text",
796
+ text: `Found ${tasks.length} tasks:\n${taskList}`
797
+ }
798
+ ]
799
+ };
800
+ }
801
+
802
+ async handleGetTask(args) {
803
+ const task = await this.motionService.getTask(args.taskId);
804
+ return {
805
+ content: [
806
+ {
807
+ type: "text",
808
+ text: `Task Details:\n- Name: ${task.name}\n- ID: ${task.id}\n- Description: ${task.description || 'N/A'}\n- Status: ${task.status || 'N/A'}\n- Priority: ${task.priority || 'N/A'}`
809
+ }
810
+ ]
811
+ };
812
+ }
813
+
814
+ async handleUpdateTask(args) {
815
+ const { taskId, ...updateData } = args;
816
+ const task = await this.motionService.updateTask(taskId, updateData);
817
+ return {
818
+ content: [
819
+ {
820
+ type: "text",
821
+ text: `Successfully updated task "${task.name}" (ID: ${task.id})`
822
+ }
823
+ ]
824
+ };
825
+ }
826
+
827
+ async handleDeleteTask(args) {
828
+ await this.motionService.deleteTask(args.taskId);
829
+ return {
830
+ content: [
831
+ {
832
+ type: "text",
833
+ text: `Successfully deleted task with ID: ${args.taskId}`
834
+ }
835
+ ]
836
+ };
837
+ }
838
+
839
+ async handleListWorkspaces() {
840
+ const workspaces = await this.motionService.getWorkspaces();
841
+ const defaultWorkspace = await this.motionService.getDefaultWorkspace();
842
+
843
+ const workspaceList = workspaces.map(w => {
844
+ const isDefault = w.id === defaultWorkspace.id ? ' (DEFAULT)' : '';
845
+ return `- ${w.name} (ID: ${w.id})${isDefault} - Type: ${w.type}`;
846
+ }).join('\n');
847
+
848
+ return {
849
+ content: [
850
+ {
851
+ type: "text",
852
+ text: `Found ${workspaces.length} workspaces:\n${workspaceList}\n\nYou can use either the workspace name or ID when specifying which workspace to work with in other commands.`
853
+ }
854
+ ]
855
+ };
856
+ }
857
+
858
+ async handleListUsers() {
859
+ const users = await this.motionService.getUsers();
860
+ const userList = users.map(u => `- ${u.name} (ID: ${u.id}) - ${u.email || 'No email'}`).join('\n');
861
+ return {
862
+ content: [
863
+ {
864
+ type: "text",
865
+ text: `Found ${users.length} users:\n${userList}`
866
+ }
867
+ ]
868
+ };
869
+ }
870
+
871
+ async handleGetContext(args) {
872
+ const includeRecentActivity = args.includeRecentActivity !== false; // Default to true
873
+ const includeWorkloadSummary = args.includeWorkloadSummary !== false; // Default to true
874
+ const includeSuggestions = args.includeSuggestions !== false; // Default to true
875
+
876
+ try {
877
+ const context = await this.motionService.getContext({
878
+ includeRecentActivity,
879
+ includeWorkloadSummary,
880
+ includeSuggestions
881
+ });
882
+
883
+ let responseText = "Current Motion Context:\n";
884
+
885
+ if (context.defaultWorkspace) {
886
+ responseText += `- Default Workspace: ${context.defaultWorkspace.name} (ID: ${context.defaultWorkspace.id})\n`;
887
+ }
888
+
889
+ if (context.recentProjects && context.recentProjects.length > 0) {
890
+ responseText += `- Recent Projects:\n`;
891
+ context.recentProjects.forEach(project => {
892
+ responseText += ` - ${project.name} (ID: ${project.id})\n`;
893
+ });
894
+ }
895
+
896
+ if (context.recentTasks && context.recentTasks.length > 0) {
897
+ responseText += `- Recent Tasks:\n`;
898
+ context.recentTasks.forEach(task => {
899
+ responseText += ` - ${task.name} (ID: ${task.id}) - Status: ${task.status || 'N/A'}\n`;
900
+ });
901
+ }
902
+
903
+ if (context.suggestions && context.suggestions.length > 0) {
904
+ responseText += `- Suggestions:\n`;
905
+ context.suggestions.forEach(suggestion => {
906
+ responseText += ` - ${suggestion}\n`;
907
+ });
908
+ }
909
+
910
+ return {
911
+ content: [
912
+ {
913
+ type: "text",
914
+ text: responseText
915
+ }
916
+ ]
917
+ };
918
+ } catch (error) {
919
+ return {
920
+ content: [
921
+ {
922
+ type: "text",
923
+ text: `Failed to get context: ${error.message}`
924
+ }
925
+ ],
926
+ isError: true
927
+ };
928
+ }
929
+ }
930
+
931
+ async handleSearchContent(args) {
932
+ const { query, searchScope = "both", workspaceId, limit = 20 } = args;
933
+
934
+ // Perform the search using the Motion API
935
+ const results = await this.motionService.searchContent({
936
+ query,
937
+ searchScope,
938
+ workspaceId,
939
+ limit
940
+ });
941
+
942
+ // Format the search results for response
943
+ const formattedResults = results.map(result => {
944
+ const type = result.projectId ? "task" : "project";
945
+ return `- [${type}] ${result.name} (ID: ${result.id})`;
946
+ }).join('\n');
947
+
948
+ return {
949
+ content: [
950
+ {
951
+ type: "text",
952
+ text: `Search Results for "${query}" (Limit: ${limit}):\n${formattedResults}`
953
+ }
954
+ ]
955
+ };
956
+ }
957
+
958
+ async handleAnalyzeWorkload(args) {
959
+ const { workspaceId, timeframe = "this_week", includeProjects = true } = args;
960
+
961
+ // Analyze workload using the Motion API
962
+ const analysis = await this.motionService.analyzeWorkload({
963
+ workspaceId,
964
+ timeframe,
965
+ includeProjects
966
+ });
967
+
968
+ // Format the analysis results for response
969
+ let responseText = `Workload Analysis (${timeframe}):\n`;
970
+ responseText += `- Total Tasks: ${analysis.totalTasks}\n`;
971
+ responseText += `- Overdue Tasks: ${analysis.overdueTasks}\n`;
972
+ responseText += `- Upcoming Deadlines: ${analysis.upcomingDeadlines}\n`;
973
+ responseText += `- Task Distribution: ${JSON.stringify(analysis.taskDistribution, null, 2)}\n`;
974
+
975
+ if (includeProjects) {
976
+ responseText += `- Project Insights: ${JSON.stringify(analysis.projectInsights, null, 2)}\n`;
977
+ }
978
+
979
+ return {
980
+ content: [
981
+ {
982
+ type: "text",
983
+ text: responseText
984
+ }
985
+ ]
986
+ };
987
+ }
988
+
989
+ async handleSuggestNextActions(args) {
990
+ const { workspaceId, context, maxSuggestions = 5 } = args;
991
+
992
+ // Get current workload and tasks
993
+ const tasks = await this.motionService.getTasks({ workspaceId });
994
+ const projects = await this.motionService.getProjects(workspaceId);
995
+
996
+ // Analyze tasks and projects to suggest next actions
997
+ const suggestions = this.generateSuggestions(tasks, projects, context, maxSuggestions);
998
+
999
+ return {
1000
+ content: [
1001
+ {
1002
+ type: "text",
1003
+ text: `Suggested Next Actions:\n${suggestions.join('\n')}`
1004
+ }
1005
+ ]
1006
+ };
1007
+ }
1008
+
1009
+ generateSuggestions(tasks, projects, context, maxSuggestions) {
1010
+ // Simple heuristic: suggest based on priority and due date
1011
+ const now = new Date();
1012
+ const sortedTasks = tasks
1013
+ .filter(task => task.dueDate) // Only scheduled tasks
1014
+ .sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
1015
+
1016
+ const highPriorityTasks = sortedTasks.filter(task => task.priority === 'ASAP' || task.priority === 'HIGH');
1017
+ const upcomingDeadlines = sortedTasks.filter(task => new Date(task.dueDate) <= new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000)); // Due in 3 days
1018
+
1019
+ let suggestions = [];
1020
+
1021
+ // Suggest high priority tasks first
1022
+ suggestions.push(...highPriorityTasks.map(task => `✅ ${task.name} (ID: ${task.id}) - Due: ${task.dueDate}`));
1023
+
1024
+ // Then suggest tasks with upcoming deadlines
1025
+ suggestions.push(...upcomingDeadlines.map(task => `⏰ ${task.name} (ID: ${task.id}) - Due: ${task.dueDate}`));
1026
+
1027
+ // Add project-related suggestions if context is project review
1028
+ if (context && context.includes('project review')) {
1029
+ const stalledProjects = projects.filter(project => project.status === 'stalled' || project.status === 'on hold');
1030
+ suggestions.push(...stalledProjects.map(project => `🔄 Review project: ${project.name} (ID: ${project.id})`));
1031
+ }
1032
+
1033
+ // Limit to maxSuggestions
1034
+ return suggestions.slice(0, maxSuggestions);
1035
+ }
1036
+
1037
+ async handleCreateProjectTemplate(args) {
1038
+ const { name, templateType, workspaceId, customizations } = args;
1039
+
1040
+ // Create project with template logic
1041
+ const project = await this.motionService.createProject({
1042
+ name,
1043
+ workspaceId,
1044
+ description: `Project created from ${templateType} template`,
1045
+ status: 'active'
1046
+ });
1047
+
1048
+ // TODO: Add logic to apply template-specific settings and tasks
1049
+
1050
+ return {
1051
+ content: [
1052
+ {
1053
+ type: "text",
1054
+ text: `Successfully created project "${project.name}" with ID: ${project.id} from ${templateType} template`
1055
+ }
1056
+ ]
1057
+ };
1058
+ }
1059
+
1060
+ async handleBulkUpdateTasks(args) {
1061
+ const { taskIds, updates, workspaceId } = args;
1062
+
1063
+ // Validate workspace ID if provided
1064
+ if (workspaceId) {
1065
+ const workspace = await this.motionService.getWorkspace(workspaceId);
1066
+ if (!workspace) {
1067
+ return {
1068
+ content: [
1069
+ {
1070
+ type: "text",
1071
+ text: `Invalid workspace ID: ${workspaceId}`
1072
+ }
1073
+ ],
1074
+ isError: true
1075
+ };
1076
+ }
1077
+ }
1078
+
1079
+ // Perform the bulk update
1080
+ await this.motionService.bulkUpdateTasks(taskIds, updates);
1081
+
1082
+ return {
1083
+ content: [
1084
+ {
1085
+ type: "text",
1086
+ text: `Successfully updated ${taskIds.length} tasks`
1087
+ }
1088
+ ]
1089
+ };
1090
+ }
1091
+
1092
+ async handleSmartScheduleTasks(args) {
1093
+ const { taskIds, workspaceId, schedulingPreferences } = args;
1094
+
1095
+ // Resolve workspace ID if not provided
1096
+ if (!workspaceId) {
1097
+ const defaultWorkspace = await this.motionService.getDefaultWorkspace();
1098
+ workspaceId = defaultWorkspace.id;
1099
+ }
1100
+
1101
+ // Perform smart scheduling
1102
+ const schedule = await this.motionService.smartScheduleTasks(taskIds, workspaceId, schedulingPreferences);
1103
+
1104
+ let responseText = `Scheduled ${taskIds.length} tasks:\n`;
1105
+ schedule.forEach(s => {
1106
+ responseText += `- Task ID: ${s.taskId} -> Scheduled Time: ${s.scheduledTime}\n`;
1107
+ });
1108
+
1109
+ return {
1110
+ content: [
1111
+ {
1112
+ type: "text",
1113
+ text: responseText
1114
+ }
1115
+ ]
1116
+ };
1117
+ }
1118
+
1119
+ async run() {
1120
+ await this.initialize();
1121
+
1122
+ const transport = new StdioServerTransport();
1123
+ await this.server.connect(transport);
1124
+
1125
+ console.error("Motion MCP Server running on stdio");
1126
+ }
1127
+ }
1128
+
1129
+ if (require.main === module) {
1130
+ const server = new MotionMCPServer();
1131
+ server.run().catch((error) => {
1132
+ console.error("Failed to run server:", error);
1133
+ process.exit(1);
1134
+ });
1135
+ }
1136
+
1137
+ module.exports = MotionMCPServer;