@sprintdock/backend 0.4.2
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/CHANGELOG.md +88 -0
- package/README.md +252 -0
- package/SERVER.md +25 -0
- package/dist/index.d.ts +1536 -0
- package/dist/index.js +4103 -0
- package/drizzle/0000_fresh_roxanne_simpson.sql +51 -0
- package/drizzle/0001_sprint_markdown_content.sql +1 -0
- package/drizzle/0002_task_touched_files.sql +8 -0
- package/drizzle/meta/0000_snapshot.json +372 -0
- package/drizzle/meta/0001_snapshot.json +379 -0
- package/drizzle/meta/_journal.json +27 -0
- package/drizzle.config.ts +14 -0
- package/package.json +40 -0
- package/src/application/container.ts +44 -0
- package/src/application/dto/plan-sprint-analytics.dto.ts +30 -0
- package/src/application/plan.service.ts +123 -0
- package/src/application/sprint.service.ts +118 -0
- package/src/application/task.service.ts +389 -0
- package/src/db/connection.ts +25 -0
- package/src/db/migrator.ts +46 -0
- package/src/db/schema/index.ts +14 -0
- package/src/db/schema/plans.ts +18 -0
- package/src/db/schema/relations.ts +36 -0
- package/src/db/schema/sprints.ts +33 -0
- package/src/db/schema/tasks.ts +62 -0
- package/src/domain/entities/index.ts +30 -0
- package/src/domain/entities/plan.entity.ts +33 -0
- package/src/domain/entities/sprint.entity.ts +44 -0
- package/src/domain/entities/task.entity.ts +80 -0
- package/src/domain/repositories/index.ts +9 -0
- package/src/domain/repositories/plan.repository.ts +21 -0
- package/src/domain/repositories/sprint.repository.ts +19 -0
- package/src/domain/repositories/task.repository.ts +35 -0
- package/src/domain/services/index.ts +9 -0
- package/src/domain/services/plan-domain.service.ts +44 -0
- package/src/domain/services/sprint-domain.service.ts +44 -0
- package/src/domain/services/task-domain.service.ts +136 -0
- package/src/errors/backend-errors.ts +75 -0
- package/src/http/app-factory.ts +55 -0
- package/src/http/controllers/health.controller.ts +33 -0
- package/src/http/controllers/plan.controller.ts +153 -0
- package/src/http/controllers/sprint.controller.ts +111 -0
- package/src/http/controllers/task.controller.ts +158 -0
- package/src/http/express-augmentation.d.ts +20 -0
- package/src/http/middleware/cors.ts +41 -0
- package/src/http/middleware/error-handler.ts +50 -0
- package/src/http/middleware/request-id.ts +28 -0
- package/src/http/middleware/validate.ts +54 -0
- package/src/http/routes/v1/index.ts +39 -0
- package/src/http/routes/v1/plan.routes.ts +51 -0
- package/src/http/routes/v1/schemas.ts +175 -0
- package/src/http/routes/v1/sprint.routes.ts +49 -0
- package/src/http/routes/v1/task.routes.ts +64 -0
- package/src/index.ts +34 -0
- package/src/infrastructure/observability/audit-log.ts +34 -0
- package/src/infrastructure/observability/request-correlation.ts +20 -0
- package/src/infrastructure/repositories/drizzle/drizzle-plan.repository.ts +138 -0
- package/src/infrastructure/repositories/drizzle/drizzle-sprint.repository.ts +137 -0
- package/src/infrastructure/repositories/drizzle/drizzle-task.repository.ts +403 -0
- package/src/infrastructure/repositories/drizzle/index.ts +16 -0
- package/src/infrastructure/repositories/drizzle/row-mappers.ts +106 -0
- package/src/infrastructure/repositories/drizzle/sqlite-db.ts +13 -0
- package/src/infrastructure/repositories/repository-factory.ts +54 -0
- package/src/infrastructure/security/auth-context.ts +35 -0
- package/src/infrastructure/security/input-guard.ts +21 -0
- package/src/infrastructure/security/rate-limiter.ts +65 -0
- package/src/mcp/bootstrap-sprintdock-sqlite.ts +45 -0
- package/src/mcp/mcp-query-helpers.ts +89 -0
- package/src/mcp/mcp-text-formatters.ts +204 -0
- package/src/mcp/mcp-tool-error.ts +24 -0
- package/src/mcp/plugins/context-tools.plugin.ts +107 -0
- package/src/mcp/plugins/default-plugins.ts +23 -0
- package/src/mcp/plugins/index.ts +21 -0
- package/src/mcp/plugins/mcp-tool-kit.ts +90 -0
- package/src/mcp/plugins/plan-tools.plugin.ts +426 -0
- package/src/mcp/plugins/sprint-tools.plugin.ts +396 -0
- package/src/mcp/plugins/task-tools.plugin.ts +528 -0
- package/src/mcp/plugins/task-workflow.plugin.ts +275 -0
- package/src/mcp/plugins/types.ts +45 -0
- package/src/mcp/register-sprintdock-mcp-tools.ts +50 -0
- package/src/mcp/sprintdock-mcp-capabilities.ts +14 -0
- package/src/mcp/sprintdock-mcp-runtime.ts +119 -0
- package/src/mcp/tool-guard.ts +58 -0
- package/src/mcp/transports/http-app-factory.ts +31 -0
- package/src/mcp/transports/http-entry.ts +27 -0
- package/src/mcp/transports/stdio-entry.ts +17 -0
- package/tests/application/container.test.ts +36 -0
- package/tests/application/plan.service.test.ts +114 -0
- package/tests/application/sprint.service.test.ts +138 -0
- package/tests/application/task.service.test.ts +325 -0
- package/tests/db/test-db.test.ts +112 -0
- package/tests/domain/plan-domain.service.test.ts +44 -0
- package/tests/domain/sprint-domain.service.test.ts +38 -0
- package/tests/domain/task-domain.service.test.ts +105 -0
- package/tests/errors/backend-errors.test.ts +44 -0
- package/tests/helpers/test-db.ts +43 -0
- package/tests/http/error-handler.test.ts +37 -0
- package/tests/http/plan.routes.test.ts +128 -0
- package/tests/http/sprint.routes.test.ts +72 -0
- package/tests/http/task.routes.test.ts +130 -0
- package/tests/http/test-app.ts +17 -0
- package/tests/infrastructure/drizzle-plan.repository.test.ts +62 -0
- package/tests/infrastructure/drizzle-sprint.repository.test.ts +49 -0
- package/tests/infrastructure/drizzle-task.repository.test.ts +132 -0
- package/tests/mcp/mcp-text-formatters.test.ts +246 -0
- package/tests/mcp/register-sprintdock-mcp-tools.test.ts +207 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: MCP plugin — core task CRUD, list, assign, move, and field updates.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import {
|
|
8
|
+
ensureSprintInPlan,
|
|
9
|
+
ensureTaskInPlan,
|
|
10
|
+
getSprintForPlan,
|
|
11
|
+
resolvePlanOrActive
|
|
12
|
+
} from "../mcp-query-helpers.js";
|
|
13
|
+
import { formatTaskList, shortId } from "../mcp-text-formatters.js";
|
|
14
|
+
import { toMcpToolError } from "../mcp-tool-error.js";
|
|
15
|
+
import { guardToolExecution, writeToolAudit } from "../tool-guard.js";
|
|
16
|
+
import type { SprintdockMcpToolPlugin } from "./types.js";
|
|
17
|
+
|
|
18
|
+
const TOUCHED_FILE_TYPE = z.enum([
|
|
19
|
+
"test",
|
|
20
|
+
"implementation",
|
|
21
|
+
"doc",
|
|
22
|
+
"config",
|
|
23
|
+
"other"
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const touchedFilesSchema = z
|
|
27
|
+
.array(
|
|
28
|
+
z.object({
|
|
29
|
+
path: z.string().min(1).max(2048),
|
|
30
|
+
fileType: TOUCHED_FILE_TYPE
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
.optional();
|
|
34
|
+
|
|
35
|
+
export const taskToolsPlugin: SprintdockMcpToolPlugin = {
|
|
36
|
+
id: "sprintdock/task-tools",
|
|
37
|
+
register(server, ctx) {
|
|
38
|
+
const { deps, kit } = ctx;
|
|
39
|
+
const {
|
|
40
|
+
jsonStructured,
|
|
41
|
+
applyPagination,
|
|
42
|
+
paginationNote,
|
|
43
|
+
optionalPlanSlug,
|
|
44
|
+
paginationSchema,
|
|
45
|
+
TASK_STATUS_SCHEMA,
|
|
46
|
+
TASK_PRIORITY_SCHEMA
|
|
47
|
+
} = kit;
|
|
48
|
+
const { planService, sprintService, taskService } = deps.services;
|
|
49
|
+
|
|
50
|
+
server.registerTool(
|
|
51
|
+
"create_task",
|
|
52
|
+
{
|
|
53
|
+
description:
|
|
54
|
+
"Creates a task in a sprint. Pass sprintIdOrSlug: full sprint UUID or sprint slug under the resolved plan (same pattern as get_sprint). Do not read SQLite directly. Order defaults to the end of the sprint. Optional dependsOnTaskIds must reference tasks in the same plan. Optional touchedFiles lists repo paths with file types (test, implementation, doc, config, other). Guarded mutation.",
|
|
55
|
+
inputSchema: z
|
|
56
|
+
.object({
|
|
57
|
+
title: z.string().min(1),
|
|
58
|
+
description: z.string().default(""),
|
|
59
|
+
priority: TASK_PRIORITY_SCHEMA.default("medium"),
|
|
60
|
+
sprintIdOrSlug: z.string().min(1),
|
|
61
|
+
assignee: z.string().nullable().optional(),
|
|
62
|
+
tags: z.array(z.string()).default([]),
|
|
63
|
+
order: z.number().int().optional(),
|
|
64
|
+
dependsOnTaskIds: z.array(z.string()).optional(),
|
|
65
|
+
touchedFiles: touchedFilesSchema,
|
|
66
|
+
planSlug: optionalPlanSlug
|
|
67
|
+
})
|
|
68
|
+
.strict()
|
|
69
|
+
},
|
|
70
|
+
async (input) => {
|
|
71
|
+
try {
|
|
72
|
+
const guard = await guardToolExecution(
|
|
73
|
+
deps.authContextResolver,
|
|
74
|
+
deps.requestCorrelation,
|
|
75
|
+
deps.rateLimiter,
|
|
76
|
+
"create_task"
|
|
77
|
+
);
|
|
78
|
+
const plan = await resolvePlanOrActive(planService, input.planSlug);
|
|
79
|
+
const sprint = await getSprintForPlan(
|
|
80
|
+
sprintService,
|
|
81
|
+
plan,
|
|
82
|
+
input.sprintIdOrSlug
|
|
83
|
+
);
|
|
84
|
+
const task = await taskService.createTask(
|
|
85
|
+
{
|
|
86
|
+
title: input.title,
|
|
87
|
+
description: input.description || null,
|
|
88
|
+
priority: input.priority ?? "medium",
|
|
89
|
+
sprintId: sprint.id,
|
|
90
|
+
assignee: input.assignee ?? null,
|
|
91
|
+
tags: input.tags ?? [],
|
|
92
|
+
order: input.order,
|
|
93
|
+
dependsOnTaskIds: input.dependsOnTaskIds,
|
|
94
|
+
touchedFiles: input.touchedFiles
|
|
95
|
+
},
|
|
96
|
+
input.planSlug
|
|
97
|
+
);
|
|
98
|
+
writeToolAudit(
|
|
99
|
+
deps.auditLog,
|
|
100
|
+
"create_task",
|
|
101
|
+
guard.principalId,
|
|
102
|
+
guard.correlationId,
|
|
103
|
+
task.id
|
|
104
|
+
);
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: "text",
|
|
109
|
+
text: `Created task '${task.title}' (id: ${shortId(task.id)}).`
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
structuredContent: jsonStructured(task)
|
|
113
|
+
};
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return toMcpToolError(e);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
server.registerTool(
|
|
121
|
+
"list_tasks",
|
|
122
|
+
{
|
|
123
|
+
description:
|
|
124
|
+
"Lists tasks for an optional sprint UUID or across the whole plan when sprintId is omitted, with optional filters (status, priority, assignee). Optional limit/offset paginate; omit both for the full filtered list.",
|
|
125
|
+
inputSchema: z
|
|
126
|
+
.object({
|
|
127
|
+
sprintId: z.string().optional(),
|
|
128
|
+
status: TASK_STATUS_SCHEMA.optional(),
|
|
129
|
+
priority: TASK_PRIORITY_SCHEMA.optional(),
|
|
130
|
+
assignee: z.string().optional(),
|
|
131
|
+
planSlug: optionalPlanSlug,
|
|
132
|
+
...paginationSchema
|
|
133
|
+
})
|
|
134
|
+
.strict()
|
|
135
|
+
},
|
|
136
|
+
async (input) => {
|
|
137
|
+
try {
|
|
138
|
+
const filter =
|
|
139
|
+
input.status || input.priority || input.assignee !== undefined
|
|
140
|
+
? {
|
|
141
|
+
...(input.status ? { status: input.status } : {}),
|
|
142
|
+
...(input.priority ? { priority: input.priority } : {}),
|
|
143
|
+
...(input.assignee !== undefined
|
|
144
|
+
? { assignee: input.assignee }
|
|
145
|
+
: {})
|
|
146
|
+
}
|
|
147
|
+
: undefined;
|
|
148
|
+
const tasksAll = await taskService.listTasks(
|
|
149
|
+
input.sprintId,
|
|
150
|
+
input.planSlug,
|
|
151
|
+
filter
|
|
152
|
+
);
|
|
153
|
+
const { page, total } = applyPagination(
|
|
154
|
+
tasksAll,
|
|
155
|
+
input.limit,
|
|
156
|
+
input.offset
|
|
157
|
+
);
|
|
158
|
+
const text =
|
|
159
|
+
formatTaskList(page) +
|
|
160
|
+
paginationNote(total, input.limit, input.offset);
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text }],
|
|
163
|
+
structuredContent: jsonStructured({ tasks: page, total })
|
|
164
|
+
};
|
|
165
|
+
} catch (e) {
|
|
166
|
+
return toMcpToolError(e);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
server.registerTool(
|
|
172
|
+
"get_task",
|
|
173
|
+
{
|
|
174
|
+
description:
|
|
175
|
+
"Returns one task by UUID with title, status, priority, assignee, tags, order, and sprint. When planSlug is set, verifies the task belongs to that plan.",
|
|
176
|
+
inputSchema: z
|
|
177
|
+
.object({
|
|
178
|
+
taskId: z.string().min(1),
|
|
179
|
+
planSlug: optionalPlanSlug
|
|
180
|
+
})
|
|
181
|
+
.strict()
|
|
182
|
+
},
|
|
183
|
+
async (input) => {
|
|
184
|
+
try {
|
|
185
|
+
if (input.planSlug !== undefined && input.planSlug !== "") {
|
|
186
|
+
const plan = await planService.resolvePlan(input.planSlug);
|
|
187
|
+
await ensureTaskInPlan(
|
|
188
|
+
taskService,
|
|
189
|
+
sprintService,
|
|
190
|
+
plan,
|
|
191
|
+
input.taskId
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const task = await taskService.getTask(input.taskId);
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: `Task '${task.title}' currently has status '${task.status}'.`
|
|
200
|
+
}
|
|
201
|
+
],
|
|
202
|
+
structuredContent: jsonStructured(task)
|
|
203
|
+
};
|
|
204
|
+
} catch (e) {
|
|
205
|
+
return toMcpToolError(e);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
server.registerTool(
|
|
211
|
+
"update_task_status",
|
|
212
|
+
{
|
|
213
|
+
description:
|
|
214
|
+
"Updates a task status with domain transition rules (e.g. done cannot jump back arbitrarily). Guarded mutation; returns the updated task.",
|
|
215
|
+
inputSchema: z
|
|
216
|
+
.object({
|
|
217
|
+
taskId: z.string().min(1),
|
|
218
|
+
status: TASK_STATUS_SCHEMA,
|
|
219
|
+
planSlug: optionalPlanSlug
|
|
220
|
+
})
|
|
221
|
+
.strict()
|
|
222
|
+
},
|
|
223
|
+
async (input) => {
|
|
224
|
+
try {
|
|
225
|
+
const guard = await guardToolExecution(
|
|
226
|
+
deps.authContextResolver,
|
|
227
|
+
deps.requestCorrelation,
|
|
228
|
+
deps.rateLimiter,
|
|
229
|
+
"update_task_status"
|
|
230
|
+
);
|
|
231
|
+
if (input.planSlug !== undefined && input.planSlug !== "") {
|
|
232
|
+
const plan = await planService.resolvePlan(input.planSlug);
|
|
233
|
+
await ensureTaskInPlan(
|
|
234
|
+
taskService,
|
|
235
|
+
sprintService,
|
|
236
|
+
plan,
|
|
237
|
+
input.taskId
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const task = await taskService.updateTaskStatus(
|
|
241
|
+
input.taskId,
|
|
242
|
+
input.status
|
|
243
|
+
);
|
|
244
|
+
writeToolAudit(
|
|
245
|
+
deps.auditLog,
|
|
246
|
+
"update_task_status",
|
|
247
|
+
guard.principalId,
|
|
248
|
+
guard.correlationId,
|
|
249
|
+
task.id,
|
|
250
|
+
{ status: task.status }
|
|
251
|
+
);
|
|
252
|
+
return {
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: `Task '${task.title}' updated to '${task.status}'. (id: ${shortId(task.id)})`
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
structuredContent: jsonStructured(task)
|
|
260
|
+
};
|
|
261
|
+
} catch (e) {
|
|
262
|
+
return toMcpToolError(e);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
server.registerTool(
|
|
268
|
+
"assign_task",
|
|
269
|
+
{
|
|
270
|
+
description:
|
|
271
|
+
"Sets or clears the assignee string on a task (null clears). Guarded mutation; scoped with planSlug when provided.",
|
|
272
|
+
inputSchema: z
|
|
273
|
+
.object({
|
|
274
|
+
taskId: z.string().min(1),
|
|
275
|
+
assignee: z.string().nullable(),
|
|
276
|
+
planSlug: optionalPlanSlug
|
|
277
|
+
})
|
|
278
|
+
.strict()
|
|
279
|
+
},
|
|
280
|
+
async (input) => {
|
|
281
|
+
try {
|
|
282
|
+
const guard = await guardToolExecution(
|
|
283
|
+
deps.authContextResolver,
|
|
284
|
+
deps.requestCorrelation,
|
|
285
|
+
deps.rateLimiter,
|
|
286
|
+
"assign_task"
|
|
287
|
+
);
|
|
288
|
+
if (input.planSlug !== undefined && input.planSlug !== "") {
|
|
289
|
+
const plan = await planService.resolvePlan(input.planSlug);
|
|
290
|
+
await ensureTaskInPlan(
|
|
291
|
+
taskService,
|
|
292
|
+
sprintService,
|
|
293
|
+
plan,
|
|
294
|
+
input.taskId
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const task = await taskService.assignTask(
|
|
298
|
+
input.taskId,
|
|
299
|
+
input.assignee
|
|
300
|
+
);
|
|
301
|
+
writeToolAudit(
|
|
302
|
+
deps.auditLog,
|
|
303
|
+
"assign_task",
|
|
304
|
+
guard.principalId,
|
|
305
|
+
guard.correlationId,
|
|
306
|
+
task.id,
|
|
307
|
+
{ assignee: task.assignee }
|
|
308
|
+
);
|
|
309
|
+
const who = task.assignee ?? "unassigned";
|
|
310
|
+
return {
|
|
311
|
+
content: [
|
|
312
|
+
{
|
|
313
|
+
type: "text",
|
|
314
|
+
text: `Task '${task.title}' assigned to '${who}'. (id: ${shortId(task.id)})`
|
|
315
|
+
}
|
|
316
|
+
],
|
|
317
|
+
structuredContent: jsonStructured(task)
|
|
318
|
+
};
|
|
319
|
+
} catch (e) {
|
|
320
|
+
return toMcpToolError(e);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
server.registerTool(
|
|
326
|
+
"move_task",
|
|
327
|
+
{
|
|
328
|
+
description:
|
|
329
|
+
"Moves a task to another sprint in the same plan (target sprint UUID). Guarded mutation; confirms the task and target sprint share a plan when planSlug is provided.",
|
|
330
|
+
inputSchema: z
|
|
331
|
+
.object({
|
|
332
|
+
taskId: z.string().min(1),
|
|
333
|
+
targetSprintId: z.string().min(1),
|
|
334
|
+
planSlug: optionalPlanSlug
|
|
335
|
+
})
|
|
336
|
+
.strict()
|
|
337
|
+
},
|
|
338
|
+
async (input) => {
|
|
339
|
+
try {
|
|
340
|
+
const guard = await guardToolExecution(
|
|
341
|
+
deps.authContextResolver,
|
|
342
|
+
deps.requestCorrelation,
|
|
343
|
+
deps.rateLimiter,
|
|
344
|
+
"move_task"
|
|
345
|
+
);
|
|
346
|
+
if (input.planSlug !== undefined && input.planSlug !== "") {
|
|
347
|
+
const plan = await planService.resolvePlan(input.planSlug);
|
|
348
|
+
await ensureTaskInPlan(
|
|
349
|
+
taskService,
|
|
350
|
+
sprintService,
|
|
351
|
+
plan,
|
|
352
|
+
input.taskId
|
|
353
|
+
);
|
|
354
|
+
await ensureSprintInPlan(
|
|
355
|
+
sprintService,
|
|
356
|
+
plan,
|
|
357
|
+
input.targetSprintId
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const targetSprint = await sprintService.getSprint(
|
|
361
|
+
input.targetSprintId
|
|
362
|
+
);
|
|
363
|
+
const task = await taskService.moveTask(
|
|
364
|
+
input.taskId,
|
|
365
|
+
input.targetSprintId
|
|
366
|
+
);
|
|
367
|
+
writeToolAudit(
|
|
368
|
+
deps.auditLog,
|
|
369
|
+
"move_task",
|
|
370
|
+
guard.principalId,
|
|
371
|
+
guard.correlationId,
|
|
372
|
+
task.id,
|
|
373
|
+
{ targetSprintId: task.sprintId }
|
|
374
|
+
);
|
|
375
|
+
return {
|
|
376
|
+
content: [
|
|
377
|
+
{
|
|
378
|
+
type: "text",
|
|
379
|
+
text: `Task '${task.title}' moved to sprint '${targetSprint.name}' (${targetSprint.slug}). (id: ${shortId(task.id)})`
|
|
380
|
+
}
|
|
381
|
+
],
|
|
382
|
+
structuredContent: jsonStructured(task)
|
|
383
|
+
};
|
|
384
|
+
} catch (e) {
|
|
385
|
+
return toMcpToolError(e);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
server.registerTool(
|
|
391
|
+
"delete_task",
|
|
392
|
+
{
|
|
393
|
+
description:
|
|
394
|
+
"Permanently deletes a task by UUID and removes its dependency edges. Guarded mutation. Use delete_sprint to remove a whole sprint. Returns confirmation with the task title and short id.",
|
|
395
|
+
inputSchema: z
|
|
396
|
+
.object({
|
|
397
|
+
taskId: z.string().min(1),
|
|
398
|
+
planSlug: optionalPlanSlug
|
|
399
|
+
})
|
|
400
|
+
.strict()
|
|
401
|
+
},
|
|
402
|
+
async (input) => {
|
|
403
|
+
try {
|
|
404
|
+
const guard = await guardToolExecution(
|
|
405
|
+
deps.authContextResolver,
|
|
406
|
+
deps.requestCorrelation,
|
|
407
|
+
deps.rateLimiter,
|
|
408
|
+
"delete_task"
|
|
409
|
+
);
|
|
410
|
+
if (input.planSlug !== undefined && input.planSlug !== "") {
|
|
411
|
+
const plan = await planService.resolvePlan(input.planSlug);
|
|
412
|
+
await ensureTaskInPlan(
|
|
413
|
+
taskService,
|
|
414
|
+
sprintService,
|
|
415
|
+
plan,
|
|
416
|
+
input.taskId
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const before = await taskService.getTask(input.taskId);
|
|
420
|
+
await taskService.deleteTask(input.taskId);
|
|
421
|
+
writeToolAudit(
|
|
422
|
+
deps.auditLog,
|
|
423
|
+
"delete_task",
|
|
424
|
+
guard.principalId,
|
|
425
|
+
guard.correlationId,
|
|
426
|
+
input.taskId,
|
|
427
|
+
{ title: before.title }
|
|
428
|
+
);
|
|
429
|
+
return {
|
|
430
|
+
content: [
|
|
431
|
+
{
|
|
432
|
+
type: "text",
|
|
433
|
+
text: `Task '${before.title}' (${shortId(before.id)}) deleted.`
|
|
434
|
+
}
|
|
435
|
+
],
|
|
436
|
+
structuredContent: jsonStructured({
|
|
437
|
+
deleted: true,
|
|
438
|
+
taskId: input.taskId
|
|
439
|
+
})
|
|
440
|
+
};
|
|
441
|
+
} catch (e) {
|
|
442
|
+
return toMcpToolError(e);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
server.registerTool(
|
|
448
|
+
"update_task",
|
|
449
|
+
{
|
|
450
|
+
description:
|
|
451
|
+
"Updates task title, description, priority, tags, sort order, or touchedFiles (replaces all file rows when set). Does not change status (use update_task_status). Guarded mutation.",
|
|
452
|
+
inputSchema: z
|
|
453
|
+
.object({
|
|
454
|
+
taskId: z.string().min(1),
|
|
455
|
+
title: z.string().min(1).optional(),
|
|
456
|
+
description: z.string().nullable().optional(),
|
|
457
|
+
priority: TASK_PRIORITY_SCHEMA.optional(),
|
|
458
|
+
tags: z.array(z.string()).optional(),
|
|
459
|
+
order: z.number().int().optional(),
|
|
460
|
+
touchedFiles: touchedFilesSchema,
|
|
461
|
+
planSlug: optionalPlanSlug
|
|
462
|
+
})
|
|
463
|
+
.strict()
|
|
464
|
+
.refine(
|
|
465
|
+
(v) =>
|
|
466
|
+
v.title !== undefined ||
|
|
467
|
+
v.description !== undefined ||
|
|
468
|
+
v.priority !== undefined ||
|
|
469
|
+
v.tags !== undefined ||
|
|
470
|
+
v.order !== undefined ||
|
|
471
|
+
v.touchedFiles !== undefined,
|
|
472
|
+
{ message: "Provide at least one field to update." }
|
|
473
|
+
)
|
|
474
|
+
},
|
|
475
|
+
async (input) => {
|
|
476
|
+
try {
|
|
477
|
+
const guard = await guardToolExecution(
|
|
478
|
+
deps.authContextResolver,
|
|
479
|
+
deps.requestCorrelation,
|
|
480
|
+
deps.rateLimiter,
|
|
481
|
+
"update_task"
|
|
482
|
+
);
|
|
483
|
+
if (input.planSlug !== undefined && input.planSlug !== "") {
|
|
484
|
+
const plan = await planService.resolvePlan(input.planSlug);
|
|
485
|
+
await ensureTaskInPlan(
|
|
486
|
+
taskService,
|
|
487
|
+
sprintService,
|
|
488
|
+
plan,
|
|
489
|
+
input.taskId
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const updated = await taskService.updateTask(input.taskId, {
|
|
493
|
+
...(input.title !== undefined ? { title: input.title } : {}),
|
|
494
|
+
...(input.description !== undefined
|
|
495
|
+
? { description: input.description }
|
|
496
|
+
: {}),
|
|
497
|
+
...(input.priority !== undefined
|
|
498
|
+
? { priority: input.priority }
|
|
499
|
+
: {}),
|
|
500
|
+
...(input.tags !== undefined ? { tags: input.tags } : {}),
|
|
501
|
+
...(input.order !== undefined ? { order: input.order } : {}),
|
|
502
|
+
...(input.touchedFiles !== undefined
|
|
503
|
+
? { touchedFiles: input.touchedFiles }
|
|
504
|
+
: {})
|
|
505
|
+
});
|
|
506
|
+
writeToolAudit(
|
|
507
|
+
deps.auditLog,
|
|
508
|
+
"update_task",
|
|
509
|
+
guard.principalId,
|
|
510
|
+
guard.correlationId,
|
|
511
|
+
updated.id
|
|
512
|
+
);
|
|
513
|
+
return {
|
|
514
|
+
content: [
|
|
515
|
+
{
|
|
516
|
+
type: "text",
|
|
517
|
+
text: `Task '${updated.title}' updated. (id: ${shortId(updated.id)})`
|
|
518
|
+
}
|
|
519
|
+
],
|
|
520
|
+
structuredContent: jsonStructured(updated)
|
|
521
|
+
};
|
|
522
|
+
} catch (e) {
|
|
523
|
+
return toMcpToolError(e);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
};
|