@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,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: MCP plugin — sprint CRUD, list with counts, and status updates.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import {
|
|
8
|
+
countByTaskStatus,
|
|
9
|
+
getSprintForPlan,
|
|
10
|
+
resolvePlanOrActive
|
|
11
|
+
} from "../mcp-query-helpers.js";
|
|
12
|
+
import { formatSprintList, shortId } from "../mcp-text-formatters.js";
|
|
13
|
+
import { toMcpToolError } from "../mcp-tool-error.js";
|
|
14
|
+
import { guardToolExecution, writeToolAudit } from "../tool-guard.js";
|
|
15
|
+
import type { SprintdockMcpToolPlugin } from "./types.js";
|
|
16
|
+
|
|
17
|
+
export const sprintToolsPlugin: SprintdockMcpToolPlugin = {
|
|
18
|
+
id: "sprintdock/sprint-tools",
|
|
19
|
+
register(server, ctx) {
|
|
20
|
+
const { deps, kit } = ctx;
|
|
21
|
+
const {
|
|
22
|
+
jsonStructured,
|
|
23
|
+
applyPagination,
|
|
24
|
+
paginationNote,
|
|
25
|
+
optionalPlanSlug,
|
|
26
|
+
paginationSchema
|
|
27
|
+
} = kit;
|
|
28
|
+
const { planService, sprintService, taskService } = deps.services;
|
|
29
|
+
|
|
30
|
+
server.registerTool(
|
|
31
|
+
"create_sprint",
|
|
32
|
+
{
|
|
33
|
+
description:
|
|
34
|
+
"Creates a sprint under the active or named plan with slug, name, goal, and optional start/end dates. New sprints start in planned status.",
|
|
35
|
+
inputSchema: z
|
|
36
|
+
.object({
|
|
37
|
+
sprintSlug: z.string().min(1),
|
|
38
|
+
name: z.string().min(1),
|
|
39
|
+
goal: z.string().min(1),
|
|
40
|
+
markdownContent: z.string().nullable().optional(),
|
|
41
|
+
startDate: z.string().datetime().nullable().optional(),
|
|
42
|
+
endDate: z.string().datetime().nullable().optional(),
|
|
43
|
+
planSlug: optionalPlanSlug
|
|
44
|
+
})
|
|
45
|
+
.strict()
|
|
46
|
+
},
|
|
47
|
+
async (input) => {
|
|
48
|
+
try {
|
|
49
|
+
const guard = await guardToolExecution(
|
|
50
|
+
deps.authContextResolver,
|
|
51
|
+
deps.requestCorrelation,
|
|
52
|
+
deps.rateLimiter,
|
|
53
|
+
"create_sprint"
|
|
54
|
+
);
|
|
55
|
+
const plan = await resolvePlanOrActive(planService, input.planSlug);
|
|
56
|
+
const sprint = await sprintService.createSprint(plan.slug, {
|
|
57
|
+
slug: input.sprintSlug,
|
|
58
|
+
planId: plan.id,
|
|
59
|
+
name: input.name,
|
|
60
|
+
goal: input.goal,
|
|
61
|
+
markdownContent: input.markdownContent ?? null,
|
|
62
|
+
startDate: input.startDate ?? null,
|
|
63
|
+
endDate: input.endDate ?? null
|
|
64
|
+
});
|
|
65
|
+
writeToolAudit(
|
|
66
|
+
deps.auditLog,
|
|
67
|
+
"create_sprint",
|
|
68
|
+
guard.principalId,
|
|
69
|
+
guard.correlationId,
|
|
70
|
+
sprint.id
|
|
71
|
+
);
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: `Created sprint '${sprint.name}' (${sprint.slug}, id: ${shortId(sprint.id)}).`
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
structuredContent: jsonStructured(sprint)
|
|
80
|
+
};
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return toMcpToolError(e);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
server.registerTool(
|
|
88
|
+
"list_sprints",
|
|
89
|
+
{
|
|
90
|
+
description:
|
|
91
|
+
"Lists sprints for the resolved plan with slug, status, name, and live task counts by status. Optional limit/offset paginate; omit both for the full list.",
|
|
92
|
+
inputSchema: z
|
|
93
|
+
.object({ planSlug: optionalPlanSlug, ...paginationSchema })
|
|
94
|
+
.strict()
|
|
95
|
+
},
|
|
96
|
+
async (input) => {
|
|
97
|
+
try {
|
|
98
|
+
const plan = await resolvePlanOrActive(planService, input.planSlug);
|
|
99
|
+
const sprints = await sprintService.listSprints(plan.id);
|
|
100
|
+
const withCounts = [];
|
|
101
|
+
for (const s of sprints) {
|
|
102
|
+
const tasks = await taskService.listTasks(s.id);
|
|
103
|
+
withCounts.push({
|
|
104
|
+
...s,
|
|
105
|
+
sprintSlug: s.slug,
|
|
106
|
+
taskCounts: countByTaskStatus(tasks)
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const { page, total } = applyPagination(
|
|
110
|
+
withCounts,
|
|
111
|
+
input.limit,
|
|
112
|
+
input.offset
|
|
113
|
+
);
|
|
114
|
+
const text =
|
|
115
|
+
formatSprintList(plan.slug, page) +
|
|
116
|
+
paginationNote(total, input.limit, input.offset) +
|
|
117
|
+
"\nPass each line's slug or `sprintId:<uuid>` as create_task / bulk_create_tasks **sprintIdOrSlug** (with the same planSlug). Full UUIDs are in structuredContent.sprints[].id.";
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: "text", text }],
|
|
120
|
+
structuredContent: jsonStructured({ sprints: page, total })
|
|
121
|
+
};
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return toMcpToolError(e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
server.registerTool(
|
|
129
|
+
"get_sprint",
|
|
130
|
+
{
|
|
131
|
+
description:
|
|
132
|
+
"Loads sprint metadata (name, goal, dates, status) by UUID or sprint slug under the resolved plan. Does not include tasks; use get_sprint_detail for tasks.",
|
|
133
|
+
inputSchema: z
|
|
134
|
+
.object({
|
|
135
|
+
sprintIdOrSlug: z.string().min(1),
|
|
136
|
+
planSlug: optionalPlanSlug
|
|
137
|
+
})
|
|
138
|
+
.strict()
|
|
139
|
+
},
|
|
140
|
+
async (input) => {
|
|
141
|
+
try {
|
|
142
|
+
const plan = await resolvePlanOrActive(planService, input.planSlug);
|
|
143
|
+
const sprint = await getSprintForPlan(
|
|
144
|
+
sprintService,
|
|
145
|
+
plan,
|
|
146
|
+
input.sprintIdOrSlug
|
|
147
|
+
);
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: "text",
|
|
152
|
+
text: `Sprint '${sprint.name}' (${sprint.slug}) is in status '${sprint.status}' (id: ${shortId(sprint.id)}).`
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
structuredContent: jsonStructured({
|
|
156
|
+
...sprint,
|
|
157
|
+
sprintSlug: sprint.slug
|
|
158
|
+
})
|
|
159
|
+
};
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return toMcpToolError(e);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
server.registerTool(
|
|
167
|
+
"update_sprint_status",
|
|
168
|
+
{
|
|
169
|
+
description:
|
|
170
|
+
"Moves a sprint through its lifecycle (planned, active, completed, archived) with domain validation. Accepts sprint UUID or slug scoped to the plan.",
|
|
171
|
+
inputSchema: z
|
|
172
|
+
.object({
|
|
173
|
+
sprintIdOrSlug: z.string().min(1),
|
|
174
|
+
status: z.enum(["planned", "active", "completed", "archived"]),
|
|
175
|
+
planSlug: optionalPlanSlug
|
|
176
|
+
})
|
|
177
|
+
.strict()
|
|
178
|
+
},
|
|
179
|
+
async (input) => {
|
|
180
|
+
try {
|
|
181
|
+
const guard = await guardToolExecution(
|
|
182
|
+
deps.authContextResolver,
|
|
183
|
+
deps.requestCorrelation,
|
|
184
|
+
deps.rateLimiter,
|
|
185
|
+
"update_sprint_status"
|
|
186
|
+
);
|
|
187
|
+
const plan = await resolvePlanOrActive(planService, input.planSlug);
|
|
188
|
+
const resolved = await getSprintForPlan(
|
|
189
|
+
sprintService,
|
|
190
|
+
plan,
|
|
191
|
+
input.sprintIdOrSlug
|
|
192
|
+
);
|
|
193
|
+
const sprint = await sprintService.updateSprintStatus(
|
|
194
|
+
resolved.id,
|
|
195
|
+
input.status
|
|
196
|
+
);
|
|
197
|
+
writeToolAudit(
|
|
198
|
+
deps.auditLog,
|
|
199
|
+
"update_sprint_status",
|
|
200
|
+
guard.principalId,
|
|
201
|
+
guard.correlationId,
|
|
202
|
+
sprint.id,
|
|
203
|
+
{ status: sprint.status }
|
|
204
|
+
);
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: `Sprint '${sprint.name}' (${sprint.slug}) updated to '${sprint.status}'.`
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
structuredContent: jsonStructured(sprint)
|
|
213
|
+
};
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return toMcpToolError(e);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
server.registerTool(
|
|
221
|
+
"delete_sprint",
|
|
222
|
+
{
|
|
223
|
+
description:
|
|
224
|
+
"Deletes a sprint and all of its tasks (cascade). Accepts sprint UUID or slug under the resolved plan. Guarded mutation.",
|
|
225
|
+
inputSchema: z
|
|
226
|
+
.object({
|
|
227
|
+
sprintIdOrSlug: z.string().min(1),
|
|
228
|
+
planSlug: optionalPlanSlug
|
|
229
|
+
})
|
|
230
|
+
.strict()
|
|
231
|
+
},
|
|
232
|
+
async (input) => {
|
|
233
|
+
try {
|
|
234
|
+
const guard = await guardToolExecution(
|
|
235
|
+
deps.authContextResolver,
|
|
236
|
+
deps.requestCorrelation,
|
|
237
|
+
deps.rateLimiter,
|
|
238
|
+
"delete_sprint"
|
|
239
|
+
);
|
|
240
|
+
const plan = await resolvePlanOrActive(planService, input.planSlug);
|
|
241
|
+
const sprint = await getSprintForPlan(
|
|
242
|
+
sprintService,
|
|
243
|
+
plan,
|
|
244
|
+
input.sprintIdOrSlug
|
|
245
|
+
);
|
|
246
|
+
await sprintService.deleteSprint(sprint.id);
|
|
247
|
+
writeToolAudit(
|
|
248
|
+
deps.auditLog,
|
|
249
|
+
"delete_sprint",
|
|
250
|
+
guard.principalId,
|
|
251
|
+
guard.correlationId,
|
|
252
|
+
sprint.id,
|
|
253
|
+
{ slug: sprint.slug }
|
|
254
|
+
);
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: "text",
|
|
259
|
+
text: `Sprint '${sprint.name}' (${sprint.slug}, id: ${shortId(sprint.id)}) deleted.`
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
structuredContent: jsonStructured({
|
|
263
|
+
deleted: true,
|
|
264
|
+
sprintId: sprint.id
|
|
265
|
+
})
|
|
266
|
+
};
|
|
267
|
+
} catch (e) {
|
|
268
|
+
return toMcpToolError(e);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
server.registerTool(
|
|
274
|
+
"update_sprint",
|
|
275
|
+
{
|
|
276
|
+
description:
|
|
277
|
+
"Updates sprint name, goal, start/end dates, or order without changing lifecycle status (use update_sprint_status for status). Accepts sprint UUID or slug. Guarded mutation.",
|
|
278
|
+
inputSchema: z
|
|
279
|
+
.object({
|
|
280
|
+
sprintIdOrSlug: z.string().min(1),
|
|
281
|
+
name: z.string().min(1).optional(),
|
|
282
|
+
goal: z.string().min(1).optional(),
|
|
283
|
+
markdownContent: z.string().nullable().optional(),
|
|
284
|
+
startDate: z.string().datetime().nullable().optional(),
|
|
285
|
+
endDate: z.string().datetime().nullable().optional(),
|
|
286
|
+
order: z.number().int().optional(),
|
|
287
|
+
planSlug: optionalPlanSlug
|
|
288
|
+
})
|
|
289
|
+
.strict()
|
|
290
|
+
.refine(
|
|
291
|
+
(v) =>
|
|
292
|
+
v.name !== undefined ||
|
|
293
|
+
v.goal !== undefined ||
|
|
294
|
+
v.markdownContent !== undefined ||
|
|
295
|
+
v.startDate !== undefined ||
|
|
296
|
+
v.endDate !== undefined ||
|
|
297
|
+
v.order !== undefined,
|
|
298
|
+
{ message: "Provide at least one field to update." }
|
|
299
|
+
)
|
|
300
|
+
},
|
|
301
|
+
async (input) => {
|
|
302
|
+
try {
|
|
303
|
+
const guard = await guardToolExecution(
|
|
304
|
+
deps.authContextResolver,
|
|
305
|
+
deps.requestCorrelation,
|
|
306
|
+
deps.rateLimiter,
|
|
307
|
+
"update_sprint"
|
|
308
|
+
);
|
|
309
|
+
const plan = await resolvePlanOrActive(planService, input.planSlug);
|
|
310
|
+
const sprint = await getSprintForPlan(
|
|
311
|
+
sprintService,
|
|
312
|
+
plan,
|
|
313
|
+
input.sprintIdOrSlug
|
|
314
|
+
);
|
|
315
|
+
const updated = await sprintService.updateSprint(sprint.id, {
|
|
316
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
317
|
+
...(input.goal !== undefined ? { goal: input.goal } : {}),
|
|
318
|
+
...(input.markdownContent !== undefined
|
|
319
|
+
? { markdownContent: input.markdownContent }
|
|
320
|
+
: {}),
|
|
321
|
+
...(input.startDate !== undefined
|
|
322
|
+
? { startDate: input.startDate }
|
|
323
|
+
: {}),
|
|
324
|
+
...(input.endDate !== undefined ? { endDate: input.endDate } : {}),
|
|
325
|
+
...(input.order !== undefined ? { order: input.order } : {})
|
|
326
|
+
});
|
|
327
|
+
writeToolAudit(
|
|
328
|
+
deps.auditLog,
|
|
329
|
+
"update_sprint",
|
|
330
|
+
guard.principalId,
|
|
331
|
+
guard.correlationId,
|
|
332
|
+
updated.id
|
|
333
|
+
);
|
|
334
|
+
return {
|
|
335
|
+
content: [
|
|
336
|
+
{
|
|
337
|
+
type: "text",
|
|
338
|
+
text: `Sprint '${updated.name}' (${updated.slug}) updated. (id: ${shortId(updated.id)})`
|
|
339
|
+
}
|
|
340
|
+
],
|
|
341
|
+
structuredContent: jsonStructured(updated)
|
|
342
|
+
};
|
|
343
|
+
} catch (e) {
|
|
344
|
+
return toMcpToolError(e);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
server.registerTool(
|
|
350
|
+
"update_sprint_markdown",
|
|
351
|
+
{
|
|
352
|
+
description:
|
|
353
|
+
"Overwrites sprint markdown notes (GFM) in SQLite for a sprint under the active or named plan. Use list_sprints / get_sprint_detail to resolve slugs. Guarded mutation.",
|
|
354
|
+
inputSchema: z
|
|
355
|
+
.object({
|
|
356
|
+
sprintIdOrSlug: z.string().min(1),
|
|
357
|
+
content: z.string(),
|
|
358
|
+
planSlug: optionalPlanSlug
|
|
359
|
+
})
|
|
360
|
+
.strict()
|
|
361
|
+
},
|
|
362
|
+
async (input) => {
|
|
363
|
+
try {
|
|
364
|
+
const guard = await guardToolExecution(
|
|
365
|
+
deps.authContextResolver,
|
|
366
|
+
deps.requestCorrelation,
|
|
367
|
+
deps.rateLimiter,
|
|
368
|
+
"update_sprint_markdown"
|
|
369
|
+
);
|
|
370
|
+
const plan = await resolvePlanOrActive(planService, input.planSlug);
|
|
371
|
+
const sprint = await getSprintForPlan(
|
|
372
|
+
sprintService,
|
|
373
|
+
plan,
|
|
374
|
+
input.sprintIdOrSlug
|
|
375
|
+
);
|
|
376
|
+
const updated = await sprintService.updateSprint(sprint.id, {
|
|
377
|
+
markdownContent: input.content
|
|
378
|
+
});
|
|
379
|
+
writeToolAudit(
|
|
380
|
+
deps.auditLog,
|
|
381
|
+
"update_sprint_markdown",
|
|
382
|
+
guard.principalId,
|
|
383
|
+
guard.correlationId,
|
|
384
|
+
updated.id
|
|
385
|
+
);
|
|
386
|
+
return {
|
|
387
|
+
content: [{ type: "text", text: "Sprint markdown updated." }],
|
|
388
|
+
structuredContent: jsonStructured({ ok: true, sprint: updated })
|
|
389
|
+
};
|
|
390
|
+
} catch (e) {
|
|
391
|
+
return toMcpToolError(e);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
};
|