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

jacob-hartmann / quire-mcp / 21649823045

03 Feb 2026 10:11PM UTC coverage: 86.299% (-4.6%) from 90.943%
21649823045

push

github

jacob-hartmann
feat(tests): enhance property-based tests and update QuireClient methods

- Refactored property-based tests in `property.test.ts` to improve string matching logic and ensure accurate character counting for HTML escaping.
- Updated `QuireClient` methods to change parameter names from `keyword` to `text` for clarity in task search functionalities.
- Enhanced test utilities and documentation for better readability and maintainability.
- Adjusted various test cases to reflect changes in method signatures and expected behaviors.

866 of 1015 branches covered (85.32%)

Branch coverage included in aggregate %.

14 of 15 new or added lines in 3 files covered. (93.33%)

68 existing lines in 8 files now uncovered.

1685 of 1941 relevant lines covered (86.81%)

30.7 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
      annotations: {
43
        readOnlyHint: true,
44
      },
45
    },
46
    async ({ projectId, parentTaskOid }, extra) => {
47
      const clientResult = await getQuireClient(extra);
4✔
48
      if (!clientResult.success) {
4✔
49
        return formatAuthError(clientResult.error);
1✔
50
      }
51

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

60
      return formatSuccess(result.data);
2✔
61
    }
62
  );
63

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

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

111
      if (!result.success) {
3✔
112
        return formatError(result.error, "task");
1✔
113
      }
114

115
      return formatSuccess(result.data);
2✔
116
    }
117
  );
118

119
  // Create Task
120
  server.registerTool(
27✔
121
    "quire.createTask",
122
    {
123
      description:
124
        "Create a new task in a project or as a subtask of an existing task. " +
125
        "To create a root task, provide a project ID/OID. " +
126
        "To create a subtask, provide a parent task OID. " +
127
        "To position a task relative to another, use createTaskAfter or createTaskBefore instead.",
128
      inputSchema: z.object({
129
        projectId: z
130
          .string()
131
          .describe(
132
            "The project ID/OID to create a root task, OR a parent task OID to create a subtask"
133
          ),
134
        name: z.string().describe("The task name/title (required)"),
135
        description: z
136
          .string()
137
          .optional()
138
          .describe("Task description in markdown format"),
139
        priority: z
140
          .number()
141
          .min(-1)
142
          .max(2)
143
          .optional()
144
          .describe("Priority: -1 (low), 0 (medium), 1 (high), 2 (urgent)"),
145
        status: z
146
          .number()
147
          .min(0)
148
          .max(100)
149
          .optional()
150
          .describe("Status: 0 (to-do) to 100 (complete)"),
151
        due: z
152
          .string()
153
          .optional()
154
          .describe("Due date in ISO 8601 format (e.g., '2024-12-31')"),
155
        start: z.string().optional().describe("Start date in ISO 8601 format"),
156
        assignees: z
157
          .array(z.string())
158
          .optional()
159
          .describe("Array of user IDs to assign to this task"),
160
        tags: z.array(z.number()).optional().describe("Array of tag IDs"),
161
      }),
162
    },
163
    async (
164
      {
165
        projectId,
166
        name,
167
        description,
168
        priority,
169
        status,
170
        due,
171
        start,
172
        assignees,
173
        tags,
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
      });
192

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

201
      return formatSuccess(result.data);
2✔
202
    }
203
  );
204

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

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

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

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

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

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

360
      const result = await clientResult.client.deleteTask(oid);
2✔
361
      if (!result.success) {
2✔
362
        return formatError(result.error, "task");
1✔
363
      }
364

365
      return formatMessage(`Task ${oid} deleted successfully.`);
1✔
366
    }
367
  );
368

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

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

421
      const result = await clientResult.client.searchTasks(
2✔
422
        projectId,
423
        keyword,
424
        options
425
      );
426
      if (!result.success) {
2!
UNCOV
427
        return formatError(result.error, "task");
×
428
      }
429

430
      return formatSuccess(result.data);
2✔
431
    }
432
  );
433

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

491
      const params = buildParams({
2✔
492
        name,
493
        description,
494
        priority,
495
        status,
496
        due,
497
        start,
498
        assignees,
499
        tags,
500
      });
501

502
      const result = await clientResult.client.createTaskAfter(
2✔
503
        taskOid,
504
        params as { name: string }
505
      );
506
      if (!result.success) {
2!
UNCOV
507
        return formatError(result.error, "task");
×
508
      }
509

510
      return formatSuccess(result.data);
2✔
511
    }
512
  );
513

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

571
      const params = buildParams({
1✔
572
        name,
573
        description,
574
        priority,
575
        status,
576
        due,
577
        start,
578
        assignees,
579
        tags,
580
      });
581

582
      const result = await clientResult.client.createTaskBefore(
1✔
583
        taskOid,
584
        params as { name: string }
585
      );
586
      if (!result.success) {
1!
UNCOV
587
        return formatError(result.error, "task");
×
588
      }
589

590
      return formatSuccess(result.data);
1✔
591
    }
592
  );
593

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

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

645
      const result = await clientResult.client.searchFolderTasks(
2✔
646
        folderId,
647
        keyword,
648
        options
649
      );
650
      if (!result.success) {
2!
UNCOV
651
        return formatError(result.error, "task");
×
652
      }
653

654
      return formatSuccess(result.data);
2✔
655
    }
656
  );
657

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

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

710
      const result = await clientResult.client.searchOrganizationTasks(
2✔
711
        organizationId,
712
        keyword,
713
        options
714
      );
715
      if (!result.success) {
2!
UNCOV
716
        return formatError(result.error, "task");
×
717
      }
718

719
      return formatSuccess(result.data);
2✔
720
    }
721
  );
722
}
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