@starasia/task-management-mcp 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.
- package/README.md +230 -0
- package/dist/api.d.ts +37 -0
- package/dist/api.js +124 -0
- package/dist/api.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +22 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas.d.ts +10 -0
- package/dist/schemas.js +14 -0
- package/dist/schemas.js.map +1 -0
- package/dist/tools.d.ts +3 -0
- package/dist/tools.js +739 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +62 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TaskManagementApiError } from "./api.js";
|
|
3
|
+
import { categorySchema, dryRunSchema, paginationSchema, prioritySchema, } from "./schemas.js";
|
|
4
|
+
const json = (data) => ({
|
|
5
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
6
|
+
});
|
|
7
|
+
const taskFilters = {
|
|
8
|
+
search: z.string().optional(),
|
|
9
|
+
statusId: z.union([z.string(), z.array(z.string())]).optional(),
|
|
10
|
+
category: z.union([categorySchema, z.array(categorySchema)]).optional(),
|
|
11
|
+
priority: z.union([prioritySchema, z.array(prioritySchema)]).optional(),
|
|
12
|
+
};
|
|
13
|
+
function normalizeArray(value) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
return Array.isArray(value) ? value : [value];
|
|
17
|
+
}
|
|
18
|
+
function stripBearerPrefix(token) {
|
|
19
|
+
const trimmed = token.trim();
|
|
20
|
+
return trimmed.toLowerCase().startsWith("bearer ")
|
|
21
|
+
? trimmed.slice(7).trim()
|
|
22
|
+
: trimmed;
|
|
23
|
+
}
|
|
24
|
+
function decodeJwtSubject(token) {
|
|
25
|
+
const normalized = stripBearerPrefix(token);
|
|
26
|
+
const [, payloadSegment] = normalized.split(".");
|
|
27
|
+
if (!payloadSegment)
|
|
28
|
+
return undefined;
|
|
29
|
+
try {
|
|
30
|
+
const payload = JSON.parse(Buffer.from(payloadSegment, "base64url").toString("utf8"));
|
|
31
|
+
return typeof payload.sub === "string" && payload.sub.trim()
|
|
32
|
+
? payload.sub.trim()
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function resolveUserId(bearerToken, userId) {
|
|
40
|
+
const explicitUserId = userId?.trim();
|
|
41
|
+
if (explicitUserId)
|
|
42
|
+
return explicitUserId;
|
|
43
|
+
const jwtSubject = decodeJwtSubject(bearerToken);
|
|
44
|
+
if (jwtSubject)
|
|
45
|
+
return jwtSubject;
|
|
46
|
+
throw new Error("Unable to derive userId from bearer token JWT subject. Provide userId explicitly or use a JWT access token with a string sub claim.");
|
|
47
|
+
}
|
|
48
|
+
const authStatus = (context) => ({
|
|
49
|
+
configured: Boolean(context),
|
|
50
|
+
environment: context?.environment,
|
|
51
|
+
userId: context?.userId,
|
|
52
|
+
organizationId: context?.organizationId,
|
|
53
|
+
hasToken: Boolean(context?.bearerToken),
|
|
54
|
+
expiresAt: context?.expiresAt,
|
|
55
|
+
});
|
|
56
|
+
export function registerTools(server, api, maxBulkSize, authTtlMs) {
|
|
57
|
+
let activeContext;
|
|
58
|
+
const requireContext = () => {
|
|
59
|
+
if (!activeContext) {
|
|
60
|
+
throw new Error("Authentication context is not configured. Ask the user for bearerToken and organizationId, then call set_auth_context first. userId is derived from the token JWT sub claim unless explicitly provided.");
|
|
61
|
+
}
|
|
62
|
+
if (new Date(activeContext.expiresAt).getTime() <= Date.now()) {
|
|
63
|
+
activeContext = undefined;
|
|
64
|
+
throw new Error("Authentication context expired. Ask the user for bearerToken and organizationId, then call set_auth_context again. userId is derived from the token JWT sub claim unless explicitly provided.");
|
|
65
|
+
}
|
|
66
|
+
return activeContext;
|
|
67
|
+
};
|
|
68
|
+
server.registerTool("set_auth_context", {
|
|
69
|
+
title: "Set Auth Context",
|
|
70
|
+
description: "Set the active user authentication context for this MCP process. Ask the user for bearerToken and organizationId before calling this tool. userId is derived from the token JWT sub claim unless explicitly provided. The token is kept only in process memory and is never returned.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
bearerToken: z
|
|
73
|
+
.string()
|
|
74
|
+
.min(1)
|
|
75
|
+
.describe("Bearer token supplied by the active user. Do not log or repeat it."),
|
|
76
|
+
userId: z
|
|
77
|
+
.string()
|
|
78
|
+
.min(1)
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("Optional explicit userId override. When omitted, the MCP derives userId from the bearer token JWT sub claim."),
|
|
81
|
+
organizationId: z.string().min(1),
|
|
82
|
+
environment: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Optional label such as dev or prod. The API URL comes from the MCP instance configuration."),
|
|
86
|
+
validate: z
|
|
87
|
+
.boolean()
|
|
88
|
+
.default(true)
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("When true, verify the context with GET /spaces before saving."),
|
|
91
|
+
},
|
|
92
|
+
annotations: {
|
|
93
|
+
readOnlyHint: false,
|
|
94
|
+
destructiveHint: false,
|
|
95
|
+
idempotentHint: false,
|
|
96
|
+
openWorldHint: true,
|
|
97
|
+
},
|
|
98
|
+
}, async (args) => {
|
|
99
|
+
const bearerToken = args.bearerToken;
|
|
100
|
+
const organizationId = args.organizationId;
|
|
101
|
+
if (!bearerToken || !organizationId) {
|
|
102
|
+
throw new Error("bearerToken and organizationId are required.");
|
|
103
|
+
}
|
|
104
|
+
const nextContext = {
|
|
105
|
+
bearerToken,
|
|
106
|
+
userId: resolveUserId(bearerToken, args.userId),
|
|
107
|
+
organizationId,
|
|
108
|
+
environment: args.environment,
|
|
109
|
+
expiresAt: new Date(Date.now() + authTtlMs).toISOString(),
|
|
110
|
+
};
|
|
111
|
+
if (args.validate ?? true) {
|
|
112
|
+
try {
|
|
113
|
+
await api.get("spaces", undefined, nextContext);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof TaskManagementApiError) {
|
|
117
|
+
throw new Error(`Auth context validation failed with API status ${error.status}. Verify the bearer token, organizationId, derived/explicit userId, and environment-specific API URL.`, { cause: error });
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
activeContext = nextContext;
|
|
123
|
+
return json({
|
|
124
|
+
...authStatus(activeContext),
|
|
125
|
+
validated: args.validate ?? true,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
server.registerTool("get_auth_context_status", {
|
|
129
|
+
title: "Get Auth Context Status",
|
|
130
|
+
description: "Return whether an auth context is configured. Never returns the bearer token.",
|
|
131
|
+
inputSchema: {},
|
|
132
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
133
|
+
}, async () => json(authStatus(activeContext)));
|
|
134
|
+
server.registerTool("clear_auth_context", {
|
|
135
|
+
title: "Clear Auth Context",
|
|
136
|
+
description: "Clear the active user authentication context from process memory.",
|
|
137
|
+
inputSchema: {},
|
|
138
|
+
annotations: {
|
|
139
|
+
readOnlyHint: false,
|
|
140
|
+
destructiveHint: false,
|
|
141
|
+
idempotentHint: true,
|
|
142
|
+
openWorldHint: false,
|
|
143
|
+
},
|
|
144
|
+
}, async () => {
|
|
145
|
+
activeContext = undefined;
|
|
146
|
+
return json(authStatus(activeContext));
|
|
147
|
+
});
|
|
148
|
+
server.registerTool("list_spaces", {
|
|
149
|
+
title: "List Spaces",
|
|
150
|
+
description: "List spaces visible to the authenticated user.",
|
|
151
|
+
inputSchema: {},
|
|
152
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
153
|
+
}, async () => json(await api.get("spaces", undefined, requireContext())));
|
|
154
|
+
server.registerTool("list_projects", {
|
|
155
|
+
title: "List Projects",
|
|
156
|
+
description: "List projects in a space. includeStats returns total/complete/inProgress/toDo/overdue/memberCount when supported by the API.",
|
|
157
|
+
inputSchema: {
|
|
158
|
+
spaceId: z.string(),
|
|
159
|
+
archived: z.boolean().default(false),
|
|
160
|
+
includeStats: z.boolean().default(true),
|
|
161
|
+
},
|
|
162
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
163
|
+
}, async (args) => json(await api.get(`spaces/${args.spaceId}/projects`, { archived: args.archived, includeStats: args.includeStats }, requireContext())));
|
|
164
|
+
server.registerTool("get_project", {
|
|
165
|
+
title: "Get Project",
|
|
166
|
+
description: "Get one project by id within a space.",
|
|
167
|
+
inputSchema: { spaceId: z.string(), projectId: z.string() },
|
|
168
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
169
|
+
}, async (args) => json(await api.get(`spaces/${args.spaceId}/projects/${args.projectId}`, undefined, requireContext())));
|
|
170
|
+
server.registerTool("list_statuses", {
|
|
171
|
+
title: "List Statuses",
|
|
172
|
+
description: "List workflow statuses for a project. Useful before create_task or change_task_status.",
|
|
173
|
+
inputSchema: { projectId: z.string() },
|
|
174
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
175
|
+
}, async (args) => json(await api.get(`projects/${args.projectId}/statuses`, undefined, requireContext())));
|
|
176
|
+
server.registerTool("list_project_members", {
|
|
177
|
+
title: "List Project Members",
|
|
178
|
+
description: "List project members and their numeric projectMemberId values for assignment.",
|
|
179
|
+
inputSchema: { projectId: z.string() },
|
|
180
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
181
|
+
}, async (args) => json(await api.get(`project-members/${args.projectId}`, undefined, requireContext())));
|
|
182
|
+
server.registerTool("list_tasks", {
|
|
183
|
+
title: "List Tasks",
|
|
184
|
+
description: "List tasks in a project with filtering/sorting. Defaults to top-level tasks unless includeSubtasks is true.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
...paginationSchema,
|
|
187
|
+
...taskFilters,
|
|
188
|
+
projectId: z.string(),
|
|
189
|
+
tagIds: z.union([z.string(), z.array(z.string())]).optional(),
|
|
190
|
+
sortBy: z
|
|
191
|
+
.enum(["createdAt", "dueDate"])
|
|
192
|
+
.default("createdAt")
|
|
193
|
+
.optional(),
|
|
194
|
+
sortDir: z.enum(["asc", "desc"]).default("asc"),
|
|
195
|
+
createdBy: z.string().optional(),
|
|
196
|
+
assignedTo: z
|
|
197
|
+
.string()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe("User id, not projectMemberId."),
|
|
200
|
+
includeSubtasks: z.boolean().default(false),
|
|
201
|
+
},
|
|
202
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
203
|
+
}, async (args) => json(await api.get(`projects/${args.projectId}/tasks`, {
|
|
204
|
+
page: args.page,
|
|
205
|
+
perPage: args.perPage,
|
|
206
|
+
search: args.search,
|
|
207
|
+
statusId: normalizeArray(args.statusId),
|
|
208
|
+
category: normalizeArray(args.category),
|
|
209
|
+
priority: normalizeArray(args.priority),
|
|
210
|
+
tagIds: normalizeArray(args.tagIds),
|
|
211
|
+
sortBy: args.sortBy,
|
|
212
|
+
sortDir: args.sortDir,
|
|
213
|
+
createdBy: args.createdBy,
|
|
214
|
+
assignedTo: args.assignedTo,
|
|
215
|
+
includeSubtasks: args.includeSubtasks,
|
|
216
|
+
}, requireContext())));
|
|
217
|
+
server.registerTool("get_task", {
|
|
218
|
+
title: "Get Task",
|
|
219
|
+
description: "Get task detail including status, assignees, tags, attachments and children when returned by the API.",
|
|
220
|
+
inputSchema: { projectId: z.string(), taskId: z.string() },
|
|
221
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
222
|
+
}, async (args) => json(await api.get(`projects/${args.projectId}/tasks/${args.taskId}`, undefined, requireContext())));
|
|
223
|
+
server.registerTool("summarize_my_tasks", {
|
|
224
|
+
title: "Summarize My Tasks",
|
|
225
|
+
description: "Fetch /my-tasks and return grouped counts plus the raw task list for agent-side reporting.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
...paginationSchema,
|
|
228
|
+
...taskFilters,
|
|
229
|
+
scope: z.enum(["me", "all"]).default("me"),
|
|
230
|
+
spaceId: z.string().optional(),
|
|
231
|
+
projectId: z.string().optional(),
|
|
232
|
+
},
|
|
233
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
234
|
+
}, async (args) => {
|
|
235
|
+
const result = await api.get("my-tasks", {
|
|
236
|
+
scope: args.scope,
|
|
237
|
+
page: args.page,
|
|
238
|
+
perPage: args.perPage,
|
|
239
|
+
search: args.search,
|
|
240
|
+
statusId: normalizeArray(args.statusId),
|
|
241
|
+
category: normalizeArray(args.category),
|
|
242
|
+
priority: normalizeArray(args.priority),
|
|
243
|
+
spaceId: args.spaceId,
|
|
244
|
+
projectId: args.projectId,
|
|
245
|
+
}, requireContext());
|
|
246
|
+
return json({ summary: summarizeTasks(result.data), ...result });
|
|
247
|
+
});
|
|
248
|
+
server.registerTool("summarize_project_progress", {
|
|
249
|
+
title: "Summarize Project Progress",
|
|
250
|
+
description: "Summarize status/category/priority/overdue distribution for a project by reading all tasks up to perPage.",
|
|
251
|
+
inputSchema: {
|
|
252
|
+
projectId: z.string(),
|
|
253
|
+
perPage: z.number().int().positive().max(500).default(200),
|
|
254
|
+
},
|
|
255
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
256
|
+
}, async (args) => {
|
|
257
|
+
const result = await api.get(`projects/${args.projectId}/tasks`, { page: 1, perPage: args.perPage, includeSubtasks: true }, requireContext());
|
|
258
|
+
return json({ summary: summarizeTasks(result.data), ...result });
|
|
259
|
+
});
|
|
260
|
+
server.registerTool("create_task", {
|
|
261
|
+
title: "Create Task",
|
|
262
|
+
description: "Create a task. Uses dryRun by default; set dryRun=false to write. createdBy defaults to the active auth userId.",
|
|
263
|
+
inputSchema: {
|
|
264
|
+
...dryRunSchema,
|
|
265
|
+
projectId: z.string(),
|
|
266
|
+
title: z.string().min(1),
|
|
267
|
+
description: z.string().optional(),
|
|
268
|
+
statusId: z.string().optional(),
|
|
269
|
+
priority: prioritySchema.default("Normal"),
|
|
270
|
+
startDate: z.string().optional(),
|
|
271
|
+
dueDate: z.string().optional(),
|
|
272
|
+
parentId: z.string().optional(),
|
|
273
|
+
createdBy: z.string().min(1).optional(),
|
|
274
|
+
assigneeIds: z
|
|
275
|
+
.array(z.number().int().positive())
|
|
276
|
+
.optional()
|
|
277
|
+
.describe("ProjectMember.id values."),
|
|
278
|
+
},
|
|
279
|
+
annotations: {
|
|
280
|
+
readOnlyHint: false,
|
|
281
|
+
destructiveHint: false,
|
|
282
|
+
idempotentHint: false,
|
|
283
|
+
openWorldHint: true,
|
|
284
|
+
},
|
|
285
|
+
}, async (args) => {
|
|
286
|
+
const context = requireContext();
|
|
287
|
+
const body = taskCreateBody({
|
|
288
|
+
...args,
|
|
289
|
+
createdBy: args.createdBy ?? context.userId,
|
|
290
|
+
});
|
|
291
|
+
if (args.dryRun ?? true)
|
|
292
|
+
return json({
|
|
293
|
+
dryRun: true,
|
|
294
|
+
method: "POST",
|
|
295
|
+
path: `projects/${args.projectId}/tasks`,
|
|
296
|
+
body,
|
|
297
|
+
});
|
|
298
|
+
return json(await api.post(`projects/${args.projectId}/tasks`, body, undefined, context));
|
|
299
|
+
});
|
|
300
|
+
server.registerTool("bulk_create_tasks", {
|
|
301
|
+
title: "Bulk Create Tasks",
|
|
302
|
+
description: "Create many tasks sequentially with dry-run by default. Stops on first API error. Supports idempotency by prechecking existing title in project.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
...dryRunSchema,
|
|
305
|
+
projectId: z.string(),
|
|
306
|
+
idempotency: z.enum(["none", "title"]).default("title"),
|
|
307
|
+
tasks: z.array(z.object({
|
|
308
|
+
title: z.string().min(1),
|
|
309
|
+
description: z.string().optional(),
|
|
310
|
+
statusId: z.string().optional(),
|
|
311
|
+
priority: prioritySchema.default("Normal"),
|
|
312
|
+
startDate: z.string().optional(),
|
|
313
|
+
dueDate: z.string().optional(),
|
|
314
|
+
parentId: z.string().optional(),
|
|
315
|
+
createdBy: z.string().min(1).optional(),
|
|
316
|
+
assigneeIds: z.array(z.number().int().positive()).optional(),
|
|
317
|
+
})),
|
|
318
|
+
},
|
|
319
|
+
annotations: {
|
|
320
|
+
readOnlyHint: false,
|
|
321
|
+
destructiveHint: false,
|
|
322
|
+
idempotentHint: true,
|
|
323
|
+
openWorldHint: true,
|
|
324
|
+
},
|
|
325
|
+
}, async (args) => {
|
|
326
|
+
if (args.tasks.length > maxBulkSize)
|
|
327
|
+
throw new Error(`Bulk limit exceeded: ${args.tasks.length}/${maxBulkSize}`);
|
|
328
|
+
const context = requireContext();
|
|
329
|
+
const bodies = args.tasks.map((task) => taskCreateBody({
|
|
330
|
+
...task,
|
|
331
|
+
projectId: args.projectId,
|
|
332
|
+
createdBy: task.createdBy ?? context.userId,
|
|
333
|
+
}));
|
|
334
|
+
if (args.dryRun ?? true)
|
|
335
|
+
return json({
|
|
336
|
+
dryRun: true,
|
|
337
|
+
count: bodies.length,
|
|
338
|
+
method: "POST",
|
|
339
|
+
path: `projects/${args.projectId}/tasks`,
|
|
340
|
+
bodies,
|
|
341
|
+
});
|
|
342
|
+
const created = [];
|
|
343
|
+
const skipped = [];
|
|
344
|
+
for (const body of bodies) {
|
|
345
|
+
if ((args.idempotency ?? "title") === "title") {
|
|
346
|
+
const existing = await api.get(`projects/${args.projectId}/tasks`, { search: body.title, includeSubtasks: true, perPage: 100 }, context);
|
|
347
|
+
const match = existing.data.find((task) => task.title.trim().toLowerCase() ===
|
|
348
|
+
body.title.trim().toLowerCase());
|
|
349
|
+
if (match) {
|
|
350
|
+
skipped.push({ reason: "title_exists", task: match });
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
created.push(await api.post(`projects/${args.projectId}/tasks`, body, undefined, context));
|
|
355
|
+
}
|
|
356
|
+
return json({
|
|
357
|
+
createdCount: created.length,
|
|
358
|
+
skippedCount: skipped.length,
|
|
359
|
+
created,
|
|
360
|
+
skipped,
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
server.registerTool("update_task", {
|
|
364
|
+
title: "Update Task",
|
|
365
|
+
description: "Update mutable task fields. Uses dryRun by default.",
|
|
366
|
+
inputSchema: {
|
|
367
|
+
...dryRunSchema,
|
|
368
|
+
projectId: z.string(),
|
|
369
|
+
taskId: z.string(),
|
|
370
|
+
title: z.string().min(1).optional(),
|
|
371
|
+
description: z.string().nullable().optional(),
|
|
372
|
+
statusId: z.string().optional(),
|
|
373
|
+
priority: prioritySchema.optional(),
|
|
374
|
+
startDate: z.string().nullable().optional(),
|
|
375
|
+
dueDate: z.string().nullable().optional(),
|
|
376
|
+
parentId: z.string().nullable().optional(),
|
|
377
|
+
},
|
|
378
|
+
annotations: {
|
|
379
|
+
readOnlyHint: false,
|
|
380
|
+
destructiveHint: false,
|
|
381
|
+
idempotentHint: true,
|
|
382
|
+
openWorldHint: true,
|
|
383
|
+
},
|
|
384
|
+
}, async (args) => {
|
|
385
|
+
const context = requireContext();
|
|
386
|
+
const body = stripUndefined({
|
|
387
|
+
title: args.title,
|
|
388
|
+
description: args.description,
|
|
389
|
+
statusId: args.statusId,
|
|
390
|
+
priority: args.priority,
|
|
391
|
+
startDate: args.startDate,
|
|
392
|
+
dueDate: args.dueDate,
|
|
393
|
+
parentId: args.parentId,
|
|
394
|
+
});
|
|
395
|
+
if (args.dryRun ?? true)
|
|
396
|
+
return json({
|
|
397
|
+
dryRun: true,
|
|
398
|
+
method: "PUT",
|
|
399
|
+
path: `projects/${args.projectId}/tasks/${args.taskId}`,
|
|
400
|
+
body,
|
|
401
|
+
});
|
|
402
|
+
return json(await api.put(`projects/${args.projectId}/tasks/${args.taskId}`, body, undefined, context));
|
|
403
|
+
});
|
|
404
|
+
server.registerTool("change_task_status", {
|
|
405
|
+
title: "Change Task Status",
|
|
406
|
+
description: "Convenience wrapper to update only statusId.",
|
|
407
|
+
inputSchema: {
|
|
408
|
+
...dryRunSchema,
|
|
409
|
+
projectId: z.string(),
|
|
410
|
+
taskId: z.string(),
|
|
411
|
+
statusId: z.string(),
|
|
412
|
+
},
|
|
413
|
+
annotations: {
|
|
414
|
+
readOnlyHint: false,
|
|
415
|
+
destructiveHint: false,
|
|
416
|
+
idempotentHint: true,
|
|
417
|
+
openWorldHint: true,
|
|
418
|
+
},
|
|
419
|
+
}, async (args) => {
|
|
420
|
+
const context = requireContext();
|
|
421
|
+
const body = { statusId: args.statusId };
|
|
422
|
+
if (args.dryRun ?? true)
|
|
423
|
+
return json({
|
|
424
|
+
dryRun: true,
|
|
425
|
+
method: "PUT",
|
|
426
|
+
path: `projects/${args.projectId}/tasks/${args.taskId}`,
|
|
427
|
+
body,
|
|
428
|
+
});
|
|
429
|
+
return json(await api.put(`projects/${args.projectId}/tasks/${args.taskId}`, body, undefined, context));
|
|
430
|
+
});
|
|
431
|
+
server.registerTool("assign_task", {
|
|
432
|
+
title: "Assign Task",
|
|
433
|
+
description: "Batch assign project members to a task. projectMemberIds are numeric ProjectMember.id values.",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
...dryRunSchema,
|
|
436
|
+
taskId: z.string(),
|
|
437
|
+
projectMemberIds: z.array(z.number().int().positive()).min(1),
|
|
438
|
+
assignedBy: z.string().min(1).optional(),
|
|
439
|
+
},
|
|
440
|
+
annotations: {
|
|
441
|
+
readOnlyHint: false,
|
|
442
|
+
destructiveHint: false,
|
|
443
|
+
idempotentHint: true,
|
|
444
|
+
openWorldHint: true,
|
|
445
|
+
},
|
|
446
|
+
}, async (args) => {
|
|
447
|
+
const context = requireContext();
|
|
448
|
+
const body = {
|
|
449
|
+
projectMemberIds: args.projectMemberIds,
|
|
450
|
+
assignedBy: args.assignedBy ?? context.userId,
|
|
451
|
+
};
|
|
452
|
+
if (args.dryRun ?? true)
|
|
453
|
+
return json({
|
|
454
|
+
dryRun: true,
|
|
455
|
+
method: "POST",
|
|
456
|
+
path: `tasks/${args.taskId}/assignees/batch-assign`,
|
|
457
|
+
body,
|
|
458
|
+
});
|
|
459
|
+
return json(await api.post(`tasks/${args.taskId}/assignees/batch-assign`, body, undefined, context));
|
|
460
|
+
});
|
|
461
|
+
server.registerTool("unassign_task", {
|
|
462
|
+
title: "Unassign Task",
|
|
463
|
+
description: "Remove one project member assignee from a task. projectMemberId is the numeric ProjectMember.id value, not userId.",
|
|
464
|
+
inputSchema: {
|
|
465
|
+
...dryRunSchema,
|
|
466
|
+
taskId: z.string(),
|
|
467
|
+
projectMemberId: z.number().int().positive(),
|
|
468
|
+
},
|
|
469
|
+
annotations: {
|
|
470
|
+
readOnlyHint: false,
|
|
471
|
+
destructiveHint: true,
|
|
472
|
+
idempotentHint: true,
|
|
473
|
+
openWorldHint: true,
|
|
474
|
+
},
|
|
475
|
+
}, async (args) => {
|
|
476
|
+
const context = requireContext();
|
|
477
|
+
const body = { projectMemberId: args.projectMemberId };
|
|
478
|
+
if (args.dryRun ?? true)
|
|
479
|
+
return json({
|
|
480
|
+
dryRun: true,
|
|
481
|
+
method: "DELETE",
|
|
482
|
+
path: `tasks/${args.taskId}/assignees`,
|
|
483
|
+
body,
|
|
484
|
+
});
|
|
485
|
+
return json(await api.delete(`tasks/${args.taskId}/assignees`, body, undefined, context));
|
|
486
|
+
});
|
|
487
|
+
server.registerTool("replace_task_assignees", {
|
|
488
|
+
title: "Replace Task Assignees",
|
|
489
|
+
description: "Replace a task's assignee set so only the provided projectMemberIds remain. Adds missing members and removes extra members.",
|
|
490
|
+
inputSchema: {
|
|
491
|
+
...dryRunSchema,
|
|
492
|
+
projectId: z.string(),
|
|
493
|
+
taskId: z.string(),
|
|
494
|
+
projectMemberIds: z.array(z.number().int().positive()),
|
|
495
|
+
assignedBy: z.string().min(1).optional(),
|
|
496
|
+
},
|
|
497
|
+
annotations: {
|
|
498
|
+
readOnlyHint: false,
|
|
499
|
+
destructiveHint: true,
|
|
500
|
+
idempotentHint: true,
|
|
501
|
+
openWorldHint: true,
|
|
502
|
+
},
|
|
503
|
+
}, async (args) => {
|
|
504
|
+
const context = requireContext();
|
|
505
|
+
const before = await api.get(`projects/${args.projectId}/tasks/${args.taskId}`, undefined, context);
|
|
506
|
+
const currentProjectMemberIds = Array.from(new Set((before.data.taskAssignees ?? []).map((assignee) => assignee.projectMemberId)));
|
|
507
|
+
const targetProjectMemberIds = Array.from(new Set(args.projectMemberIds));
|
|
508
|
+
const currentSet = new Set(currentProjectMemberIds);
|
|
509
|
+
const targetSet = new Set(targetProjectMemberIds);
|
|
510
|
+
const addedProjectMemberIds = targetProjectMemberIds.filter((projectMemberId) => !currentSet.has(projectMemberId));
|
|
511
|
+
const removedProjectMemberIds = currentProjectMemberIds.filter((projectMemberId) => !targetSet.has(projectMemberId));
|
|
512
|
+
if (args.dryRun ?? true) {
|
|
513
|
+
return json({
|
|
514
|
+
dryRun: true,
|
|
515
|
+
currentProjectMemberIds,
|
|
516
|
+
targetProjectMemberIds,
|
|
517
|
+
addedProjectMemberIds,
|
|
518
|
+
removedProjectMemberIds,
|
|
519
|
+
operations: [
|
|
520
|
+
...(addedProjectMemberIds.length > 0
|
|
521
|
+
? [
|
|
522
|
+
{
|
|
523
|
+
method: "POST",
|
|
524
|
+
path: `tasks/${args.taskId}/assignees/batch-assign`,
|
|
525
|
+
body: {
|
|
526
|
+
projectMemberIds: addedProjectMemberIds,
|
|
527
|
+
assignedBy: args.assignedBy ?? context.userId,
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
]
|
|
531
|
+
: []),
|
|
532
|
+
...removedProjectMemberIds.map((projectMemberId) => ({
|
|
533
|
+
method: "DELETE",
|
|
534
|
+
path: `tasks/${args.taskId}/assignees`,
|
|
535
|
+
body: { projectMemberId },
|
|
536
|
+
})),
|
|
537
|
+
],
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
if (addedProjectMemberIds.length > 0) {
|
|
541
|
+
await api.post(`tasks/${args.taskId}/assignees/batch-assign`, {
|
|
542
|
+
projectMemberIds: addedProjectMemberIds,
|
|
543
|
+
assignedBy: args.assignedBy ?? context.userId,
|
|
544
|
+
}, undefined, context);
|
|
545
|
+
}
|
|
546
|
+
for (const projectMemberId of removedProjectMemberIds) {
|
|
547
|
+
await api.delete(`tasks/${args.taskId}/assignees`, { projectMemberId }, undefined, context);
|
|
548
|
+
}
|
|
549
|
+
const after = await api.get(`projects/${args.projectId}/tasks/${args.taskId}`, undefined, context);
|
|
550
|
+
return json({
|
|
551
|
+
currentProjectMemberIds,
|
|
552
|
+
targetProjectMemberIds,
|
|
553
|
+
addedProjectMemberIds,
|
|
554
|
+
removedProjectMemberIds,
|
|
555
|
+
finalAssigneeProjectMemberIds: (after.data.taskAssignees ?? []).map((assignee) => assignee.projectMemberId),
|
|
556
|
+
task: after.data,
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
server.registerTool("monitor_overdue_tasks", {
|
|
560
|
+
title: "Monitor Overdue Tasks",
|
|
561
|
+
description: "Find non-complete tasks with dueDate before now from /my-tasks. Use scope=all if permitted by user permission.",
|
|
562
|
+
inputSchema: {
|
|
563
|
+
scope: z.enum(["me", "all"]).default("me"),
|
|
564
|
+
projectId: z.string().optional(),
|
|
565
|
+
spaceId: z.string().optional(),
|
|
566
|
+
perPage: z.number().int().positive().max(500).default(200),
|
|
567
|
+
},
|
|
568
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
569
|
+
}, async (args) => {
|
|
570
|
+
const result = await api.get("my-tasks", {
|
|
571
|
+
scope: args.scope,
|
|
572
|
+
projectId: args.projectId,
|
|
573
|
+
spaceId: args.spaceId,
|
|
574
|
+
perPage: args.perPage,
|
|
575
|
+
}, requireContext());
|
|
576
|
+
const now = Date.now();
|
|
577
|
+
const overdue = result.data.filter((task) => task.dueDate &&
|
|
578
|
+
new Date(task.dueDate).getTime() < now &&
|
|
579
|
+
!isComplete(task));
|
|
580
|
+
return json({ count: overdue.length, tasks: overdue });
|
|
581
|
+
});
|
|
582
|
+
server.registerTool("monitor_stale_tasks", {
|
|
583
|
+
title: "Monitor Stale Tasks",
|
|
584
|
+
description: "Find non-complete tasks not updated for N days from /my-tasks.",
|
|
585
|
+
inputSchema: {
|
|
586
|
+
scope: z.enum(["me", "all"]).default("me"),
|
|
587
|
+
days: z.number().int().positive().default(7),
|
|
588
|
+
projectId: z.string().optional(),
|
|
589
|
+
spaceId: z.string().optional(),
|
|
590
|
+
perPage: z.number().int().positive().max(500).default(200),
|
|
591
|
+
},
|
|
592
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
593
|
+
}, async (args) => {
|
|
594
|
+
const result = await api.get("my-tasks", {
|
|
595
|
+
scope: args.scope,
|
|
596
|
+
projectId: args.projectId,
|
|
597
|
+
spaceId: args.spaceId,
|
|
598
|
+
perPage: args.perPage,
|
|
599
|
+
}, requireContext());
|
|
600
|
+
const cutoff = Date.now() - (args.days ?? 7) * 24 * 60 * 60 * 1000;
|
|
601
|
+
const stale = result.data.filter((task) => task.updatedAt &&
|
|
602
|
+
new Date(task.updatedAt).getTime() < cutoff &&
|
|
603
|
+
!isComplete(task));
|
|
604
|
+
return json({ count: stale.length, tasks: stale });
|
|
605
|
+
});
|
|
606
|
+
server.registerTool("workload_by_user", {
|
|
607
|
+
title: "Workload By User",
|
|
608
|
+
description: "Group tasks by assignee from /my-tasks scope=all/me.",
|
|
609
|
+
inputSchema: {
|
|
610
|
+
scope: z.enum(["me", "all"]).default("all"),
|
|
611
|
+
projectId: z.string().optional(),
|
|
612
|
+
spaceId: z.string().optional(),
|
|
613
|
+
perPage: z.number().int().positive().max(500).default(200),
|
|
614
|
+
},
|
|
615
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
616
|
+
}, async (args) => {
|
|
617
|
+
const result = await api.get("my-tasks", {
|
|
618
|
+
scope: args.scope,
|
|
619
|
+
projectId: args.projectId,
|
|
620
|
+
spaceId: args.spaceId,
|
|
621
|
+
perPage: args.perPage,
|
|
622
|
+
}, requireContext());
|
|
623
|
+
return json({ workload: workloadByUser(result.data), meta: result.meta });
|
|
624
|
+
});
|
|
625
|
+
server.registerTool("generate_work_report", {
|
|
626
|
+
title: "Generate Work Report",
|
|
627
|
+
description: "Generate a compact JSON work report from /my-tasks for daily/weekly reporting.",
|
|
628
|
+
inputSchema: {
|
|
629
|
+
scope: z.enum(["me", "all"]).default("me"),
|
|
630
|
+
projectId: z.string().optional(),
|
|
631
|
+
spaceId: z.string().optional(),
|
|
632
|
+
perPage: z.number().int().positive().max(500).default(200),
|
|
633
|
+
},
|
|
634
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
635
|
+
}, async (args) => {
|
|
636
|
+
const result = await api.get("my-tasks", {
|
|
637
|
+
scope: args.scope,
|
|
638
|
+
projectId: args.projectId,
|
|
639
|
+
spaceId: args.spaceId,
|
|
640
|
+
perPage: args.perPage,
|
|
641
|
+
}, requireContext());
|
|
642
|
+
return json({
|
|
643
|
+
generatedAt: new Date().toISOString(),
|
|
644
|
+
summary: summarizeTasks(result.data),
|
|
645
|
+
workload: workloadByUser(result.data),
|
|
646
|
+
tasks: result.data,
|
|
647
|
+
meta: result.meta,
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function taskCreateBody(args) {
|
|
652
|
+
return stripUndefined({
|
|
653
|
+
title: args.title,
|
|
654
|
+
description: args.description,
|
|
655
|
+
statusId: args.statusId,
|
|
656
|
+
priority: args.priority,
|
|
657
|
+
startDate: args.startDate,
|
|
658
|
+
dueDate: args.dueDate,
|
|
659
|
+
projectId: args.projectId,
|
|
660
|
+
parentId: args.parentId,
|
|
661
|
+
createdBy: args.createdBy,
|
|
662
|
+
assigneeIds: args.assigneeIds,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function stripUndefined(obj) {
|
|
666
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
667
|
+
}
|
|
668
|
+
function summarizeTasks(tasks) {
|
|
669
|
+
const summary = {
|
|
670
|
+
total: tasks.length,
|
|
671
|
+
byCategory: { ToDo: 0, InProgress: 0, Complete: 0 },
|
|
672
|
+
byPriority: { Low: 0, Normal: 0, High: 0, Urgent: 0 },
|
|
673
|
+
overdue: 0,
|
|
674
|
+
unassigned: 0,
|
|
675
|
+
};
|
|
676
|
+
const now = Date.now();
|
|
677
|
+
for (const task of tasks) {
|
|
678
|
+
const category = task.statusRef?.category ?? "ToDo";
|
|
679
|
+
summary.byCategory[category] += 1;
|
|
680
|
+
summary.byPriority[task.priority] += 1;
|
|
681
|
+
if (task.dueDate &&
|
|
682
|
+
new Date(task.dueDate).getTime() < now &&
|
|
683
|
+
!isComplete(task))
|
|
684
|
+
summary.overdue += 1;
|
|
685
|
+
if (!task.taskAssignees?.length)
|
|
686
|
+
summary.unassigned += 1;
|
|
687
|
+
}
|
|
688
|
+
return summary;
|
|
689
|
+
}
|
|
690
|
+
function workloadByUser(tasks) {
|
|
691
|
+
const map = new Map();
|
|
692
|
+
const now = Date.now();
|
|
693
|
+
for (const task of tasks) {
|
|
694
|
+
const assignees = task.taskAssignees?.length
|
|
695
|
+
? task.taskAssignees
|
|
696
|
+
: [
|
|
697
|
+
{
|
|
698
|
+
id: 0,
|
|
699
|
+
taskId: task.id,
|
|
700
|
+
projectMemberId: 0,
|
|
701
|
+
projectMember: {
|
|
702
|
+
id: 0,
|
|
703
|
+
projectId: task.projectId,
|
|
704
|
+
userId: "unassigned",
|
|
705
|
+
employeeName: "Unassigned",
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
];
|
|
709
|
+
for (const assignee of assignees) {
|
|
710
|
+
const userId = assignee.projectMember?.userId ??
|
|
711
|
+
`projectMember:${assignee.projectMemberId}`;
|
|
712
|
+
const current = map.get(userId) ?? {
|
|
713
|
+
userId,
|
|
714
|
+
name: assignee.projectMember?.employeeName,
|
|
715
|
+
total: 0,
|
|
716
|
+
complete: 0,
|
|
717
|
+
overdue: 0,
|
|
718
|
+
urgent: 0,
|
|
719
|
+
tasks: [],
|
|
720
|
+
};
|
|
721
|
+
current.total += 1;
|
|
722
|
+
if (task.statusRef?.category === "Complete")
|
|
723
|
+
current.complete += 1;
|
|
724
|
+
if (task.dueDate &&
|
|
725
|
+
new Date(task.dueDate).getTime() < now &&
|
|
726
|
+
!isComplete(task))
|
|
727
|
+
current.overdue += 1;
|
|
728
|
+
if (task.priority === "Urgent")
|
|
729
|
+
current.urgent += 1;
|
|
730
|
+
current.tasks.push(task);
|
|
731
|
+
map.set(userId, current);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return [...map.values()].sort((a, b) => b.total - a.total);
|
|
735
|
+
}
|
|
736
|
+
function isComplete(task) {
|
|
737
|
+
return task.statusRef?.category === "Complete";
|
|
738
|
+
}
|
|
739
|
+
//# sourceMappingURL=tools.js.map
|