• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jacob-hartmann / quire-mcp / 21574865257

02 Feb 2026 02:06AM UTC coverage: 90.943% (+83.3%) from 7.646%
21574865257

push

github

jacob-hartmann
Refactor test assertions in attachment and comment tools to improve clarity and consistency. Remove unnecessary binding of mock client methods, enhancing readability and maintainability of test cases. Update .gitignore to exclude package.json for cleaner project structure.

835 of 929 branches covered (89.88%)

Branch coverage included in aggregate %.

1615 of 1765 relevant lines covered (91.5%)

14.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.59
/src/tools/task.ts
1
/**
2
 * Task Tools
3
 *
4
 * MCP tools for managing Quire tasks.
5
 */
6

7
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
import { z } from "zod";
9
import { getQuireClient } from "../quire/client-factory.js";
10
import {
11
  formatError,
12
  formatAuthError,
13
  formatSuccess,
14
  formatMessage,
15
  formatValidationError,
16
  buildParams,
17
} from "./utils.js";
18

19
/**
20
 * Register all task tools with the MCP server
21
 */
22
export function registerTaskTools(server: McpServer): void {
23
  // List Tasks
24
  server.registerTool(
27✔
25
    "quire.listTasks",
26
    {
27
      description:
28
        "List tasks in a project. Returns root-level tasks by default, " +
29
        "or subtasks if a parent task OID is provided.",
30
      inputSchema: z.object({
31
        projectId: z
32
          .string()
33
          .describe("The project ID (e.g., 'my-project') or OID"),
34
        parentTaskOid: z
35
          .string()
36
          .optional()
37
          .describe(
38
            "Parent task OID to list subtasks of (optional). " +
39
              "If not provided, returns root-level tasks."
40
          ),
41
      }),
42
    },
43
    async ({ projectId, parentTaskOid }, extra) => {
44
      const clientResult = await getQuireClient(extra);
4✔
45
      if (!clientResult.success) {
4✔
46
        return formatAuthError(clientResult.error);
1✔
47
      }
48

49
      const result = await clientResult.client.listTasks(
3✔
50
        projectId,
51
        parentTaskOid
52
      );
53
      if (!result.success) {
3✔
54
        return formatError(result.error, "task");
1✔
55
      }
56

57
      return formatSuccess(result.data);
2✔
58
    }
59
  );
60

61
  // Get Task
62
  server.registerTool(
27✔
63
    "quire.getTask",
64
    {
65
      description:
66
        "Get detailed information about a specific task. " +
67
        "Can be retrieved by project ID + task ID, or by task OID alone.",
68
      inputSchema: z.object({
69
        projectId: z
70
          .string()
71
          .optional()
72
          .describe(
73
            "The project ID (required when using taskId, not needed when using oid)"
74
          ),
75
        taskId: z
76
          .number()
77
          .optional()
78
          .describe("The task ID number within the project"),
79
        oid: z
80
          .string()
81
          .optional()
82
          .describe(
83
            "The task OID (unique identifier). Use this OR projectId+taskId"
84
          ),
85
      }),
86
    },
87
    async ({ projectId, taskId, oid }, extra) => {
88
      const clientResult = await getQuireClient(extra);
4✔
89
      if (!clientResult.success) {
4!
90
        return formatAuthError(clientResult.error);
×
91
      }
92

93
      // Get task by OID or by projectId + taskId
94
      let result;
95
      if (oid) {
4✔
96
        result = await clientResult.client.getTask(oid);
2✔
97
      } else if (projectId && taskId !== undefined) {
2✔
98
        result = await clientResult.client.getTask(projectId, taskId);
1✔
99
      } else {
100
        return formatValidationError(
1✔
101
          "Must provide either 'oid' or both 'projectId' and 'taskId'"
102
        );
103
      }
104

105
      if (!result.success) {
3✔
106
        return formatError(result.error, "task");
1✔
107
      }
108

109
      return formatSuccess(result.data);
2✔
110
    }
111
  );
112

113
  // Create Task
114
  server.registerTool(
27✔
115
    "quire.createTask",
116
    {
117
      description:
118
        "Create a new task in a project. The task name is required; " +
119
        "all other fields are optional.",
120
      inputSchema: z.object({
121
        projectId: z
122
          .string()
123
          .describe("The project ID (e.g., 'my-project') or OID"),
124
        name: z.string().describe("The task name/title (required)"),
125
        description: z
126
          .string()
127
          .optional()
128
          .describe("Task description in markdown format"),
129
        priority: z
130
          .number()
131
          .min(-1)
132
          .max(2)
133
          .optional()
134
          .describe("Priority: -1 (low), 0 (medium), 1 (high), 2 (urgent)"),
135
        status: z
136
          .number()
137
          .min(0)
138
          .max(100)
139
          .optional()
140
          .describe("Status: 0 (to-do) to 100 (complete)"),
141
        due: z
142
          .string()
143
          .optional()
144
          .describe("Due date in ISO 8601 format (e.g., '2024-12-31')"),
145
        start: z.string().optional().describe("Start date in ISO 8601 format"),
146
        assignees: z
147
          .array(z.string())
148
          .optional()
149
          .describe("Array of user IDs to assign to this task"),
150
        tags: z.array(z.number()).optional().describe("Array of tag IDs"),
151
        parentOid: z
152
          .string()
153
          .optional()
154
          .describe("OID of parent task to create this as a subtask"),
155
        afterOid: z
156
          .string()
157
          .optional()
158
          .describe("OID of task to insert this task after"),
159
      }),
160
    },
161
    async (
162
      {
163
        projectId,
164
        name,
165
        description,
166
        priority,
167
        status,
168
        due,
169
        start,
170
        assignees,
171
        tags,
172
        parentOid,
173
        afterOid,
174
      },
175
      extra
176
    ) => {
177
      const clientResult = await getQuireClient(extra);
3✔
178
      if (!clientResult.success) {
3!
179
        return formatAuthError(clientResult.error);
×
180
      }
181

182
      const params = buildParams({
3✔
183
        name,
184
        description,
185
        priority,
186
        status,
187
        due,
188
        start,
189
        assignees,
190
        tags,
191
        parentOid,
192
        afterOid,
193
      });
194

195
      const result = await clientResult.client.createTask(
3✔
196
        projectId,
197
        params as { name: string }
198
      );
199
      if (!result.success) {
3✔
200
        return formatError(result.error, "task");
1✔
201
      }
202

203
      return formatSuccess(result.data);
2✔
204
    }
205
  );
206

207
  // Update Task
208
  server.registerTool(
27✔
209
    "quire.updateTask",
210
    {
211
      description:
212
        "Update an existing task. Can be identified by project ID + task ID, " +
213
        "or by task OID alone. Only provided fields will be updated.",
214
      inputSchema: z.object({
215
        projectId: z
216
          .string()
217
          .optional()
218
          .describe(
219
            "The project ID (required when using taskId, not needed when using oid)"
220
          ),
221
        taskId: z
222
          .number()
223
          .optional()
224
          .describe("The task ID number within the project"),
225
        oid: z
226
          .string()
227
          .optional()
228
          .describe(
229
            "The task OID (unique identifier). Use this OR projectId+taskId"
230
          ),
231
        name: z.string().optional().describe("New task name/title"),
232
        description: z
233
          .string()
234
          .optional()
235
          .describe("New task description in markdown format"),
236
        priority: z
237
          .number()
238
          .min(-1)
239
          .max(2)
240
          .optional()
241
          .describe("Priority: -1 (low), 0 (medium), 1 (high), 2 (urgent)"),
242
        status: z
243
          .number()
244
          .min(0)
245
          .max(100)
246
          .optional()
247
          .describe("Status: 0 (to-do) to 100 (complete)"),
248
        due: z
249
          .string()
250
          .optional()
251
          .describe("Due date in ISO 8601 format (e.g., '2024-12-31')"),
252
        start: z.string().optional().describe("Start date in ISO 8601 format"),
253
        assignees: z
254
          .array(z.string())
255
          .optional()
256
          .describe("Replace all assignees with this list of user IDs"),
257
        addAssignees: z
258
          .array(z.string())
259
          .optional()
260
          .describe("User IDs to add as assignees"),
261
        removeAssignees: z
262
          .array(z.string())
263
          .optional()
264
          .describe("User IDs to remove from assignees"),
265
        tags: z
266
          .array(z.number())
267
          .optional()
268
          .describe("Replace all tags with this list of tag IDs"),
269
        addTags: z.array(z.number()).optional().describe("Tag IDs to add"),
270
        removeTags: z
271
          .array(z.number())
272
          .optional()
273
          .describe("Tag IDs to remove"),
274
      }),
275
    },
276
    async (
277
      {
278
        projectId,
279
        taskId,
280
        oid,
281
        name,
282
        description,
283
        priority,
284
        status,
285
        due,
286
        start,
287
        assignees,
288
        addAssignees,
289
        removeAssignees,
290
        tags,
291
        addTags,
292
        removeTags,
293
      },
294
      extra
295
    ) => {
296
      const clientResult = await getQuireClient(extra);
4✔
297
      if (!clientResult.success) {
4!
298
        return formatAuthError(clientResult.error);
×
299
      }
300

301
      const updateParams = buildParams({
4✔
302
        name,
303
        description,
304
        priority,
305
        status,
306
        due,
307
        start,
308
        assignees,
309
        addAssignees,
310
        removeAssignees,
311
        tags,
312
        addTags,
313
        removeTags,
314
      });
315

316
      // Update task by OID or by projectId + taskId
317
      let result;
318
      if (oid) {
4✔
319
        result = await clientResult.client.updateTask(oid, updateParams);
2✔
320
      } else if (projectId && taskId !== undefined) {
2✔
321
        result = await clientResult.client.updateTask(
1✔
322
          projectId,
323
          taskId,
324
          updateParams
325
        );
326
      } else {
327
        return formatValidationError(
1✔
328
          "Must provide either 'oid' or both 'projectId' and 'taskId'"
329
        );
330
      }
331

332
      if (!result.success) {
3!
333
        return formatError(result.error, "task");
×
334
      }
335

336
      return formatSuccess(result.data);
3✔
337
    }
338
  );
339

340
  // Delete Task
341
  server.registerTool(
27✔
342
    "quire.deleteTask",
343
    {
344
      description:
345
        "Delete a task and all its subtasks. This action cannot be undone.",
346
      inputSchema: z.object({
347
        oid: z.string().describe("The task OID (unique identifier) to delete"),
348
      }),
349
    },
350
    async ({ oid }, extra) => {
351
      const clientResult = await getQuireClient(extra);
2✔
352
      if (!clientResult.success) {
2!
353
        return formatAuthError(clientResult.error);
×
354
      }
355

356
      const result = await clientResult.client.deleteTask(oid);
2✔
357
      if (!result.success) {
2✔
358
        return formatError(result.error, "task");
1✔
359
      }
360

361
      return formatMessage(`Task ${oid} deleted successfully.`);
1✔
362
    }
363
  );
364

365
  // Search Tasks
366
  server.registerTool(
27✔
367
    "quire.searchTasks",
368
    {
369
      description:
370
        "Search for tasks in a project by keyword and optional filters. " +
371
        "Returns tasks matching the search criteria.",
372
      inputSchema: z.object({
373
        projectId: z
374
          .string()
375
          .describe("The project ID (e.g., 'my-project') or OID to search in"),
376
        keyword: z
377
          .string()
378
          .describe(
379
            "Search keyword to match against task names and descriptions"
380
          ),
381
        status: z
382
          .number()
383
          .min(0)
384
          .max(100)
385
          .optional()
386
          .describe("Filter by status: 0 (to-do) to 100 (complete)"),
387
        priority: z
388
          .number()
389
          .min(-1)
390
          .max(2)
391
          .optional()
392
          .describe(
393
            "Filter by priority: -1 (low), 0 (medium), 1 (high), 2 (urgent)"
394
          ),
395
        assigneeId: z
396
          .string()
397
          .optional()
398
          .describe("Filter by assignee user ID"),
399
        tagId: z.number().optional().describe("Filter by tag ID"),
400
      }),
401
    },
402
    async (
403
      { projectId, keyword, status, priority, assigneeId, tagId },
404
      extra
405
    ) => {
406
      const clientResult = await getQuireClient(extra);
2✔
407
      if (!clientResult.success) {
2!
408
        return formatAuthError(clientResult.error);
×
409
      }
410

411
      const options = buildParams({ status, priority, assigneeId, tagId });
2✔
412

413
      const result = await clientResult.client.searchTasks(
2✔
414
        projectId,
415
        keyword,
416
        options
417
      );
418
      if (!result.success) {
2!
419
        return formatError(result.error, "task");
×
420
      }
421

422
      return formatSuccess(result.data);
2✔
423
    }
424
  );
425

426
  // Create Task After
427
  server.registerTool(
27✔
428
    "quire.createTaskAfter",
429
    {
430
      description:
431
        "Create a new task immediately after a specified task. " +
432
        "The new task will be at the same level as the reference task.",
433
      inputSchema: z.object({
434
        taskOid: z.string().describe("The OID of the task to insert after"),
435
        name: z.string().describe("The task name/title (required)"),
436
        description: z
437
          .string()
438
          .optional()
439
          .describe("Task description in markdown format"),
440
        priority: z
441
          .number()
442
          .min(-1)
443
          .max(2)
444
          .optional()
445
          .describe("Priority: -1 (low), 0 (medium), 1 (high), 2 (urgent)"),
446
        status: z
447
          .number()
448
          .min(0)
449
          .max(100)
450
          .optional()
451
          .describe("Status: 0 (to-do) to 100 (complete)"),
452
        due: z
453
          .string()
454
          .optional()
455
          .describe("Due date in ISO 8601 format (e.g., '2024-12-31')"),
456
        start: z.string().optional().describe("Start date in ISO 8601 format"),
457
        assignees: z
458
          .array(z.string())
459
          .optional()
460
          .describe("Array of user IDs to assign to this task"),
461
        tags: z.array(z.number()).optional().describe("Array of tag IDs"),
462
      }),
463
    },
464
    async (
465
      {
466
        taskOid,
467
        name,
468
        description,
469
        priority,
470
        status,
471
        due,
472
        start,
473
        assignees,
474
        tags,
475
      },
476
      extra
477
    ) => {
478
      const clientResult = await getQuireClient(extra);
2✔
479
      if (!clientResult.success) {
2!
480
        return formatAuthError(clientResult.error);
×
481
      }
482

483
      const params = buildParams({
2✔
484
        name,
485
        description,
486
        priority,
487
        status,
488
        due,
489
        start,
490
        assignees,
491
        tags,
492
      });
493

494
      const result = await clientResult.client.createTaskAfter(
2✔
495
        taskOid,
496
        params as { name: string }
497
      );
498
      if (!result.success) {
2!
499
        return formatError(result.error, "task");
×
500
      }
501

502
      return formatSuccess(result.data);
2✔
503
    }
504
  );
505

506
  // Create Task Before
507
  server.registerTool(
27✔
508
    "quire.createTaskBefore",
509
    {
510
      description:
511
        "Create a new task immediately before a specified task. " +
512
        "The new task will be at the same level as the reference task.",
513
      inputSchema: z.object({
514
        taskOid: z.string().describe("The OID of the task to insert before"),
515
        name: z.string().describe("The task name/title (required)"),
516
        description: z
517
          .string()
518
          .optional()
519
          .describe("Task description in markdown format"),
520
        priority: z
521
          .number()
522
          .min(-1)
523
          .max(2)
524
          .optional()
525
          .describe("Priority: -1 (low), 0 (medium), 1 (high), 2 (urgent)"),
526
        status: z
527
          .number()
528
          .min(0)
529
          .max(100)
530
          .optional()
531
          .describe("Status: 0 (to-do) to 100 (complete)"),
532
        due: z
533
          .string()
534
          .optional()
535
          .describe("Due date in ISO 8601 format (e.g., '2024-12-31')"),
536
        start: z.string().optional().describe("Start date in ISO 8601 format"),
537
        assignees: z
538
          .array(z.string())
539
          .optional()
540
          .describe("Array of user IDs to assign to this task"),
541
        tags: z.array(z.number()).optional().describe("Array of tag IDs"),
542
      }),
543
    },
544
    async (
545
      {
546
        taskOid,
547
        name,
548
        description,
549
        priority,
550
        status,
551
        due,
552
        start,
553
        assignees,
554
        tags,
555
      },
556
      extra
557
    ) => {
558
      const clientResult = await getQuireClient(extra);
1✔
559
      if (!clientResult.success) {
1!
560
        return formatAuthError(clientResult.error);
×
561
      }
562

563
      const params = buildParams({
1✔
564
        name,
565
        description,
566
        priority,
567
        status,
568
        due,
569
        start,
570
        assignees,
571
        tags,
572
      });
573

574
      const result = await clientResult.client.createTaskBefore(
1✔
575
        taskOid,
576
        params as { name: string }
577
      );
578
      if (!result.success) {
1!
579
        return formatError(result.error, "task");
×
580
      }
581

582
      return formatSuccess(result.data);
1✔
583
    }
584
  );
585

586
  // Search Folder Tasks
587
  server.registerTool(
27✔
588
    "quire.searchFolderTasks",
589
    {
590
      description:
591
        "Search for tasks within a specific folder by keyword and optional filters.",
592
      inputSchema: z.object({
593
        folderId: z
594
          .string()
595
          .describe("The folder ID (e.g., 'my-folder') or OID to search in"),
596
        keyword: z
597
          .string()
598
          .describe(
599
            "Search keyword to match against task names and descriptions"
600
          ),
601
        status: z
602
          .number()
603
          .min(0)
604
          .max(100)
605
          .optional()
606
          .describe("Filter by status: 0 (to-do) to 100 (complete)"),
607
        priority: z
608
          .number()
609
          .min(-1)
610
          .max(2)
611
          .optional()
612
          .describe(
613
            "Filter by priority: -1 (low), 0 (medium), 1 (high), 2 (urgent)"
614
          ),
615
        assigneeId: z
616
          .string()
617
          .optional()
618
          .describe("Filter by assignee user ID"),
619
        tagId: z.number().optional().describe("Filter by tag ID"),
620
      }),
621
    },
622
    async (
623
      { folderId, keyword, status, priority, assigneeId, tagId },
624
      extra
625
    ) => {
626
      const clientResult = await getQuireClient(extra);
2✔
627
      if (!clientResult.success) {
2!
628
        return formatAuthError(clientResult.error);
×
629
      }
630

631
      const options = buildParams({ status, priority, assigneeId, tagId });
2✔
632

633
      const result = await clientResult.client.searchFolderTasks(
2✔
634
        folderId,
635
        keyword,
636
        options
637
      );
638
      if (!result.success) {
2!
639
        return formatError(result.error, "task");
×
640
      }
641

642
      return formatSuccess(result.data);
2✔
643
    }
644
  );
645

646
  // Search Organization Tasks
647
  server.registerTool(
27✔
648
    "quire.searchOrganizationTasks",
649
    {
650
      description:
651
        "Search for tasks across an entire organization by keyword and optional filters. " +
652
        "This searches all projects within the organization.",
653
      inputSchema: z.object({
654
        organizationId: z
655
          .string()
656
          .describe("The organization ID (e.g., 'my-org') or OID to search in"),
657
        keyword: z
658
          .string()
659
          .describe(
660
            "Search keyword to match against task names and descriptions"
661
          ),
662
        status: z
663
          .number()
664
          .min(0)
665
          .max(100)
666
          .optional()
667
          .describe("Filter by status: 0 (to-do) to 100 (complete)"),
668
        priority: z
669
          .number()
670
          .min(-1)
671
          .max(2)
672
          .optional()
673
          .describe(
674
            "Filter by priority: -1 (low), 0 (medium), 1 (high), 2 (urgent)"
675
          ),
676
        assigneeId: z
677
          .string()
678
          .optional()
679
          .describe("Filter by assignee user ID"),
680
        tagId: z.number().optional().describe("Filter by tag ID"),
681
      }),
682
    },
683
    async (
684
      { organizationId, keyword, status, priority, assigneeId, tagId },
685
      extra
686
    ) => {
687
      const clientResult = await getQuireClient(extra);
2✔
688
      if (!clientResult.success) {
2!
689
        return formatAuthError(clientResult.error);
×
690
      }
691

692
      const options = buildParams({ status, priority, assigneeId, tagId });
2✔
693

694
      const result = await clientResult.client.searchOrganizationTasks(
2✔
695
        organizationId,
696
        keyword,
697
        options
698
      );
699
      if (!result.success) {
2!
700
        return formatError(result.error, "task");
×
701
      }
702

703
      return formatSuccess(result.data);
2✔
704
    }
705
  );
706
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc