@opsee/mcp-server 0.9.32 → 0.9.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/blocknote-fixtures.test.ts +58 -0
- package/src/tools/descriptions.ts +25 -0
- package/src/tools/initiatives.ts +11 -6
- package/src/tools/tasks-bulk.ts +171 -0
- package/src/tools/tasks-delete.ts +52 -0
- package/src/tools/tasks-read-write.ts +372 -0
- package/src/tools/tasks-relations.ts +159 -0
- package/src/tools/tasks.ts +10 -704
- package/src/utils/format/blocknote.ts +115 -0
- package/src/utils/format/comment.ts +19 -0
- package/src/utils/format/common.ts +32 -0
- package/src/utils/format/cycle.ts +19 -0
- package/src/utils/format/docs.ts +49 -0
- package/src/utils/format/index.ts +30 -0
- package/src/utils/format/initiative.ts +210 -0
- package/src/utils/format/label.ts +26 -0
- package/src/utils/format/milestone.ts +34 -0
- package/src/utils/format/project.ts +21 -0
- package/src/utils/format/task-dependency.ts +25 -0
- package/src/utils/format/task.ts +81 -0
- package/src/utils/format/user.ts +14 -0
- package/src/utils/format.ts +4 -634
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { blockNoteToMarkdown } from "../utils/format.js";
|
|
6
|
+
|
|
7
|
+
// The BlockNote <-> markdown mapping is implemented three times against no
|
|
8
|
+
// shared contract: this reader, orchestrator/internal/util/blocknote.go, and
|
|
9
|
+
// the frontend BlockEditor via @blocknote/core. testdata/blocknote is the
|
|
10
|
+
// language-neutral fixture set all three should eventually assert against;
|
|
11
|
+
// this suite pins the TypeScript side of it.
|
|
12
|
+
const FIXTURE_DIR = join(dirname(fileURLToPath(import.meta.url)), "../../../testdata/blocknote");
|
|
13
|
+
|
|
14
|
+
interface Fixture {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
/** Stored value when it is BlockNote JSON. Serialized before rendering. */
|
|
18
|
+
blocknote?: unknown[];
|
|
19
|
+
/** Stored value when it is not BlockNote JSON, passed through verbatim. */
|
|
20
|
+
input?: string;
|
|
21
|
+
markdown: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fixtures = readdirSync(FIXTURE_DIR)
|
|
25
|
+
.filter((f) => f.endsWith(".json"))
|
|
26
|
+
.sort()
|
|
27
|
+
.map((f) => JSON.parse(readFileSync(join(FIXTURE_DIR, f), "utf8")) as Fixture);
|
|
28
|
+
|
|
29
|
+
describe("blockNoteToMarkdown golden fixtures", () => {
|
|
30
|
+
// Naming the cases rather than counting them: a suite whose whole job is to
|
|
31
|
+
// catch drift must fail, not vacuously pass, if the fixtures go missing.
|
|
32
|
+
test("covers every construct the task-description contract promises", () => {
|
|
33
|
+
expect(fixtures.map((f) => f.name).sort()).toEqual([
|
|
34
|
+
"block-separation",
|
|
35
|
+
"checklist",
|
|
36
|
+
"code-fence",
|
|
37
|
+
"empty-document",
|
|
38
|
+
"heading-level-clamp",
|
|
39
|
+
"headings",
|
|
40
|
+
"inline-styles",
|
|
41
|
+
"links",
|
|
42
|
+
"nested-bullets",
|
|
43
|
+
"passthrough-malformed-block",
|
|
44
|
+
"passthrough-non-array-json",
|
|
45
|
+
"passthrough-plain-text",
|
|
46
|
+
"task-description",
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const fixture of fixtures) {
|
|
51
|
+
test(`${fixture.name}: ${fixture.description}`, () => {
|
|
52
|
+
const isBlockNote = fixture.blocknote !== undefined;
|
|
53
|
+
expect(isBlockNote || typeof fixture.input === "string").toBe(true);
|
|
54
|
+
const stored = isBlockNote ? JSON.stringify(fixture.blocknote) : fixture.input!;
|
|
55
|
+
expect(blockNoteToMarkdown(stored)).toBe(fixture.markdown);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Description strings shared by more than one tool or schema field.
|
|
3
|
+
*
|
|
4
|
+
* These are LLM-facing contract text: an agent picks a tool and fills its
|
|
5
|
+
* arguments from nothing but this wording. Copies that drift teach two
|
|
6
|
+
* different contracts for the same field, so anything repeated lives here.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Task/description authoring rules. Shared by create and update. */
|
|
10
|
+
export const TASK_DESCRIPTION_GUIDANCE =
|
|
11
|
+
"Markdown is supported — headings (##), bullets (-), code fences, inline **bold** / *italic* / `code` / links all render as proper formatted blocks in the UI. For ACCEPTANCE CRITERIA, write them inline in this description as a `## Acceptance criteria` heading followed by markdown checkbox lines: `- [ ] criterion text` and `- [x] done item`. The UI renders those as clickable checkboxes (BlockNote checkListItem) and users can toggle them. Do NOT use the structured opsee_add_acceptance_criterion tool for typical AC — it writes to a separate DB table that does not surface in the task detail UI; it's reserved for automation paths (PR-to-criterion mapping etc.). Written as-is to the backend; the frontend BlockEditor parses markdown on read and saves the parsed BlockNote JSON on the next edit, so the round-trip preserves structure.";
|
|
12
|
+
|
|
13
|
+
export const CREATE_TASK_DESCRIPTION = `Task description. ${TASK_DESCRIPTION_GUIDANCE}`;
|
|
14
|
+
|
|
15
|
+
export const UPDATE_TASK_DESCRIPTION = `New description. ${TASK_DESCRIPTION_GUIDANCE}`;
|
|
16
|
+
|
|
17
|
+
export const BULK_PROJECT_ID = "Project ID - all tasks must belong to this project";
|
|
18
|
+
|
|
19
|
+
export const STORY_POINTS_ESTIMATE = "Story points estimate (positive integer)";
|
|
20
|
+
|
|
21
|
+
export const INITIATIVE_TASK_REF = "Temporary reference ID used to wire edges and parent relationships";
|
|
22
|
+
|
|
23
|
+
export const INITIATIVE_TASK_PARENT_REF = "Ref of the parent task (to nest this as a subtask)";
|
|
24
|
+
|
|
25
|
+
export const INITIATIVE_EDGE_TYPE = "Dependency type: blocks | blocked_by | duplicates | relates_to";
|
package/src/tools/initiatives.ts
CHANGED
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
formatReconcileChangeset,
|
|
13
13
|
formatError,
|
|
14
14
|
} from "../utils/format.js";
|
|
15
|
+
import {
|
|
16
|
+
INITIATIVE_EDGE_TYPE,
|
|
17
|
+
INITIATIVE_TASK_PARENT_REF,
|
|
18
|
+
INITIATIVE_TASK_REF,
|
|
19
|
+
} from "./descriptions.js";
|
|
15
20
|
|
|
16
21
|
const INITIATIVE_STATUSES = ["draft", "active", "paused", "completed", "abandoned"] as const;
|
|
17
22
|
const ANCHOR_TYPES = ["none", "task", "milestone", "cycle"] as const;
|
|
@@ -212,10 +217,10 @@ export function registerInitiativeTools(
|
|
|
212
217
|
{
|
|
213
218
|
initiativeId: z.number().int().min(1).describe("The initiative ID"),
|
|
214
219
|
tasks: z.array(z.object({
|
|
215
|
-
ref: z.string().describe(
|
|
220
|
+
ref: z.string().describe(INITIATIVE_TASK_REF),
|
|
216
221
|
title: z.string().describe("Task title"),
|
|
217
222
|
description: z.string().optional().describe("Task description"),
|
|
218
|
-
parentRef: z.string().optional().describe(
|
|
223
|
+
parentRef: z.string().optional().describe(INITIATIVE_TASK_PARENT_REF),
|
|
219
224
|
taskTypeId: z.number().int().min(1).optional().describe("Task type ID"),
|
|
220
225
|
taskPriorityId: z.number().int().min(1).optional().describe("Task priority ID"),
|
|
221
226
|
boardColumnId: z.number().int().min(1).optional().describe("Board column ID (status)"),
|
|
@@ -223,7 +228,7 @@ export function registerInitiativeTools(
|
|
|
223
228
|
edges: z.array(z.object({
|
|
224
229
|
fromRef: z.string().describe("Ref of the source task"),
|
|
225
230
|
toRef: z.string().describe("Ref of the target task"),
|
|
226
|
-
type: z.enum(EDGE_TYPES).describe(
|
|
231
|
+
type: z.enum(EDGE_TYPES).describe(INITIATIVE_EDGE_TYPE),
|
|
227
232
|
})).describe("Dependency edges between tasks (by ref)"),
|
|
228
233
|
},
|
|
229
234
|
{ readOnlyHint: false, destructiveHint: false },
|
|
@@ -283,10 +288,10 @@ export function registerInitiativeTools(
|
|
|
283
288
|
{
|
|
284
289
|
initiativeId: z.number().int().min(1).describe("The initiative ID"),
|
|
285
290
|
tasks: z.array(z.object({
|
|
286
|
-
ref: z.string().describe(
|
|
291
|
+
ref: z.string().describe(INITIATIVE_TASK_REF),
|
|
287
292
|
title: z.string().describe("Task title"),
|
|
288
293
|
description: z.string().optional().describe("Task description"),
|
|
289
|
-
parentRef: z.string().optional().describe(
|
|
294
|
+
parentRef: z.string().optional().describe(INITIATIVE_TASK_PARENT_REF),
|
|
290
295
|
taskTypeId: z.number().int().min(1).optional().describe("Task type ID"),
|
|
291
296
|
taskPriorityId: z.number().int().min(1).optional().describe("Task priority ID"),
|
|
292
297
|
boardColumnId: z.number().int().min(1).optional().describe("Board column ID (status)"),
|
|
@@ -295,7 +300,7 @@ export function registerInitiativeTools(
|
|
|
295
300
|
edges: z.array(z.object({
|
|
296
301
|
fromRef: z.string().describe("Ref of the source task"),
|
|
297
302
|
toRef: z.string().describe("Ref of the target task"),
|
|
298
|
-
type: z.enum(EDGE_TYPES).describe(
|
|
303
|
+
type: z.enum(EDGE_TYPES).describe(INITIATIVE_EDGE_TYPE),
|
|
299
304
|
})).describe("Dependency edges between tasks (by ref)"),
|
|
300
305
|
dryRun: z.boolean().optional().describe("When true, compute and return the changeset without writing anything. Always call with dryRun:true first to preview."),
|
|
301
306
|
},
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { ApiClients } from "../client/api.js";
|
|
4
|
+
import { formatError } from "../utils/format.js";
|
|
5
|
+
import { BULK_PROJECT_ID } from "./descriptions.js";
|
|
6
|
+
|
|
7
|
+
// Multi-task edits. All of these go through the backend BulkEditTasks RPC,
|
|
8
|
+
// which applies every change in one transaction and silently skips task ids
|
|
9
|
+
// the caller cannot see - hence the "n of m" wording in every response.
|
|
10
|
+
export function registerTaskBulkTools(
|
|
11
|
+
server: McpServer,
|
|
12
|
+
getClients: () => ApiClients,
|
|
13
|
+
): void {
|
|
14
|
+
server.tool(
|
|
15
|
+
"opsee_bulk_update_tasks",
|
|
16
|
+
"Atomicity: backend wraps all updates in one DB transaction. Per-task lookup failures (task not visible to your company, or unknown ID) are silently skipped — compare updated_count against taskIds.length to detect partial application. Omitted fields are not touched. labelIds with a non-empty list replaces each task's labels; empty/omitted leaves them alone.",
|
|
17
|
+
{
|
|
18
|
+
projectId: z.number().int().min(1).describe(BULK_PROJECT_ID),
|
|
19
|
+
taskIds: z.array(z.number().int().min(1)).min(1).describe("Task IDs to update (at least one)"),
|
|
20
|
+
boardColumnId: z.number().int().min(1).optional().describe("New board column ID (status)"),
|
|
21
|
+
taskPriorityId: z.number().int().min(1).optional().describe("New priority ID"),
|
|
22
|
+
taskTypeId: z.number().int().min(1).optional().describe("New task type ID"),
|
|
23
|
+
assignedUserId: z.number().int().min(1).optional().describe("New assigned user ID"),
|
|
24
|
+
cycleId: z.number().int().min(1).optional().describe("New cycle/sprint ID"),
|
|
25
|
+
milestoneId: z.number().int().min(1).optional().describe("Milestone ID to attach every task to. Bulk only supports set, not clear — use opsee_update_task for detaching a single task."),
|
|
26
|
+
storyPoints: z.number().int().min(1).optional().describe("Story points to set on every task"),
|
|
27
|
+
estimatedHours: z.number().min(0).optional().describe("Estimated hours to set on every task"),
|
|
28
|
+
parentTaskId: z.number().int().min(0).optional().describe("Parent task ID for every task (pass 0 to clear)"),
|
|
29
|
+
labelIds: z.array(z.number().int().min(1)).optional().describe("Replace label attachments on every task with this set. Empty / omitted leaves labels untouched."),
|
|
30
|
+
},
|
|
31
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
32
|
+
async ({ projectId, taskIds, boardColumnId, taskPriorityId, taskTypeId, assignedUserId, cycleId, milestoneId, storyPoints, estimatedHours, parentTaskId, labelIds }) => {
|
|
33
|
+
try {
|
|
34
|
+
const clients = getClients();
|
|
35
|
+
const res = await clients.tasks.bulkEditTasks({
|
|
36
|
+
projectId,
|
|
37
|
+
taskIds,
|
|
38
|
+
boardColumnId,
|
|
39
|
+
taskPriorityId,
|
|
40
|
+
taskTypeId,
|
|
41
|
+
assignedUserId,
|
|
42
|
+
cycleId,
|
|
43
|
+
milestoneId,
|
|
44
|
+
storyPoints,
|
|
45
|
+
estimatedHours,
|
|
46
|
+
parentTaskId,
|
|
47
|
+
repositoryIds: [],
|
|
48
|
+
labelIds: labelIds ?? [],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const requested = taskIds.length;
|
|
52
|
+
const updated = res.updatedCount;
|
|
53
|
+
let text = `Updated ${updated} of ${requested} task(s).`;
|
|
54
|
+
if (updated < requested) {
|
|
55
|
+
text += " (Tasks not visible to your company or not found were skipped.)";
|
|
56
|
+
}
|
|
57
|
+
return { content: [{ type: "text", text }] };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
server.tool(
|
|
65
|
+
"opsee_bulk_move_to_cycle",
|
|
66
|
+
"Convenience wrapper: move many tasks to the same cycle in one call. Equivalent to opsee_bulk_update_tasks with only cycleId set.",
|
|
67
|
+
{
|
|
68
|
+
projectId: z.number().int().min(1).describe(BULK_PROJECT_ID),
|
|
69
|
+
taskIds: z.array(z.number().int().min(1)).min(1).describe("Task IDs to move"),
|
|
70
|
+
cycleId: z.number().int().min(1).describe("Target cycle/sprint ID"),
|
|
71
|
+
},
|
|
72
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
73
|
+
async ({ projectId, taskIds, cycleId }) => {
|
|
74
|
+
try {
|
|
75
|
+
const clients = getClients();
|
|
76
|
+
const res = await clients.tasks.bulkEditTasks({
|
|
77
|
+
projectId,
|
|
78
|
+
taskIds,
|
|
79
|
+
cycleId,
|
|
80
|
+
repositoryIds: [],
|
|
81
|
+
labelIds: [],
|
|
82
|
+
});
|
|
83
|
+
const text = `Moved ${res.updatedCount} of ${taskIds.length} task(s) to cycle ${cycleId}.`;
|
|
84
|
+
return { content: [{ type: "text", text }] };
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
server.tool(
|
|
92
|
+
"opsee_bulk_move_to_column",
|
|
93
|
+
"Convenience wrapper: move many tasks to the same board column (status) in one call.",
|
|
94
|
+
{
|
|
95
|
+
projectId: z.number().int().min(1).describe(BULK_PROJECT_ID),
|
|
96
|
+
taskIds: z.array(z.number().int().min(1)).min(1).describe("Task IDs to move"),
|
|
97
|
+
boardColumnId: z.number().int().min(1).describe("Target board column ID (status)"),
|
|
98
|
+
},
|
|
99
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
100
|
+
async ({ projectId, taskIds, boardColumnId }) => {
|
|
101
|
+
try {
|
|
102
|
+
const clients = getClients();
|
|
103
|
+
const res = await clients.tasks.bulkEditTasks({
|
|
104
|
+
projectId,
|
|
105
|
+
taskIds,
|
|
106
|
+
boardColumnId,
|
|
107
|
+
repositoryIds: [],
|
|
108
|
+
labelIds: [],
|
|
109
|
+
});
|
|
110
|
+
const text = `Moved ${res.updatedCount} of ${taskIds.length} task(s) to column ${boardColumnId}.`;
|
|
111
|
+
return { content: [{ type: "text", text }] };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
server.tool(
|
|
119
|
+
"opsee_bulk_assign",
|
|
120
|
+
"Convenience wrapper: assign many tasks to the same user in one call.",
|
|
121
|
+
{
|
|
122
|
+
projectId: z.number().int().min(1).describe(BULK_PROJECT_ID),
|
|
123
|
+
taskIds: z.array(z.number().int().min(1)).min(1).describe("Task IDs to reassign"),
|
|
124
|
+
assigneeId: z.number().int().min(1).describe("Target user ID"),
|
|
125
|
+
},
|
|
126
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
127
|
+
async ({ projectId, taskIds, assigneeId }) => {
|
|
128
|
+
try {
|
|
129
|
+
const clients = getClients();
|
|
130
|
+
const res = await clients.tasks.bulkEditTasks({
|
|
131
|
+
projectId,
|
|
132
|
+
taskIds,
|
|
133
|
+
assignedUserId: assigneeId,
|
|
134
|
+
repositoryIds: [],
|
|
135
|
+
labelIds: [],
|
|
136
|
+
});
|
|
137
|
+
const text = `Assigned ${res.updatedCount} of ${taskIds.length} task(s) to user ${assigneeId}.`;
|
|
138
|
+
return { content: [{ type: "text", text }] };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
server.tool(
|
|
146
|
+
"opsee_bulk_set_priority",
|
|
147
|
+
"Convenience wrapper: set the same priority on many tasks in one call.",
|
|
148
|
+
{
|
|
149
|
+
projectId: z.number().int().min(1).describe(BULK_PROJECT_ID),
|
|
150
|
+
taskIds: z.array(z.number().int().min(1)).min(1).describe("Task IDs to reprioritize"),
|
|
151
|
+
priorityId: z.number().int().min(1).describe("Target priority ID (from opsee_list_task_priorities)"),
|
|
152
|
+
},
|
|
153
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
154
|
+
async ({ projectId, taskIds, priorityId }) => {
|
|
155
|
+
try {
|
|
156
|
+
const clients = getClients();
|
|
157
|
+
const res = await clients.tasks.bulkEditTasks({
|
|
158
|
+
projectId,
|
|
159
|
+
taskIds,
|
|
160
|
+
taskPriorityId: priorityId,
|
|
161
|
+
repositoryIds: [],
|
|
162
|
+
labelIds: [],
|
|
163
|
+
});
|
|
164
|
+
const text = `Set priority ${priorityId} on ${res.updatedCount} of ${taskIds.length} task(s).`;
|
|
165
|
+
return { content: [{ type: "text", text }] };
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { ApiClients } from "../client/api.js";
|
|
4
|
+
import { formatError } from "../utils/format.js";
|
|
5
|
+
import { BULK_PROJECT_ID } from "./descriptions.js";
|
|
6
|
+
|
|
7
|
+
// Task deletion, single and bulk.
|
|
8
|
+
export function registerTaskDeleteTools(
|
|
9
|
+
server: McpServer,
|
|
10
|
+
getClients: () => ApiClients,
|
|
11
|
+
): void {
|
|
12
|
+
server.tool(
|
|
13
|
+
"opsee_delete_task",
|
|
14
|
+
"Delete a task by ID. This is permanent.",
|
|
15
|
+
{ taskId: z.number().int().min(1).describe("The task ID to delete") },
|
|
16
|
+
{ readOnlyHint: false, destructiveHint: true },
|
|
17
|
+
async ({ taskId }) => {
|
|
18
|
+
try {
|
|
19
|
+
const clients = getClients();
|
|
20
|
+
await clients.tasks.deleteTask({ id: taskId });
|
|
21
|
+
return { content: [{ type: "text", text: "Task deleted." }] };
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
server.tool(
|
|
29
|
+
"opsee_bulk_delete_tasks",
|
|
30
|
+
"Delete multiple tasks in one call. Tasks not visible to your company or not found are silently skipped — compare deletedCount against taskIds.length to detect partial application. This is permanent.",
|
|
31
|
+
{
|
|
32
|
+
projectId: z.number().int().min(1).describe(BULK_PROJECT_ID),
|
|
33
|
+
taskIds: z.array(z.number().int().min(1)).min(1).describe("Task IDs to delete (at least one)"),
|
|
34
|
+
},
|
|
35
|
+
{ readOnlyHint: false, destructiveHint: true },
|
|
36
|
+
async ({ projectId, taskIds }) => {
|
|
37
|
+
try {
|
|
38
|
+
const clients = getClients();
|
|
39
|
+
const res = await clients.tasks.bulkDeleteTasks({ projectId, taskIds });
|
|
40
|
+
const requested = taskIds.length;
|
|
41
|
+
const deleted = res.deletedCount;
|
|
42
|
+
let text = `Deleted ${deleted} of ${requested} task(s).`;
|
|
43
|
+
if (deleted < requested) {
|
|
44
|
+
text += " (Tasks not visible to your company or not found were skipped.)";
|
|
45
|
+
}
|
|
46
|
+
return { content: [{ type: "text", text }] };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
}
|