@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.
Files changed (107) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +252 -0
  3. package/SERVER.md +25 -0
  4. package/dist/index.d.ts +1536 -0
  5. package/dist/index.js +4103 -0
  6. package/drizzle/0000_fresh_roxanne_simpson.sql +51 -0
  7. package/drizzle/0001_sprint_markdown_content.sql +1 -0
  8. package/drizzle/0002_task_touched_files.sql +8 -0
  9. package/drizzle/meta/0000_snapshot.json +372 -0
  10. package/drizzle/meta/0001_snapshot.json +379 -0
  11. package/drizzle/meta/_journal.json +27 -0
  12. package/drizzle.config.ts +14 -0
  13. package/package.json +40 -0
  14. package/src/application/container.ts +44 -0
  15. package/src/application/dto/plan-sprint-analytics.dto.ts +30 -0
  16. package/src/application/plan.service.ts +123 -0
  17. package/src/application/sprint.service.ts +118 -0
  18. package/src/application/task.service.ts +389 -0
  19. package/src/db/connection.ts +25 -0
  20. package/src/db/migrator.ts +46 -0
  21. package/src/db/schema/index.ts +14 -0
  22. package/src/db/schema/plans.ts +18 -0
  23. package/src/db/schema/relations.ts +36 -0
  24. package/src/db/schema/sprints.ts +33 -0
  25. package/src/db/schema/tasks.ts +62 -0
  26. package/src/domain/entities/index.ts +30 -0
  27. package/src/domain/entities/plan.entity.ts +33 -0
  28. package/src/domain/entities/sprint.entity.ts +44 -0
  29. package/src/domain/entities/task.entity.ts +80 -0
  30. package/src/domain/repositories/index.ts +9 -0
  31. package/src/domain/repositories/plan.repository.ts +21 -0
  32. package/src/domain/repositories/sprint.repository.ts +19 -0
  33. package/src/domain/repositories/task.repository.ts +35 -0
  34. package/src/domain/services/index.ts +9 -0
  35. package/src/domain/services/plan-domain.service.ts +44 -0
  36. package/src/domain/services/sprint-domain.service.ts +44 -0
  37. package/src/domain/services/task-domain.service.ts +136 -0
  38. package/src/errors/backend-errors.ts +75 -0
  39. package/src/http/app-factory.ts +55 -0
  40. package/src/http/controllers/health.controller.ts +33 -0
  41. package/src/http/controllers/plan.controller.ts +153 -0
  42. package/src/http/controllers/sprint.controller.ts +111 -0
  43. package/src/http/controllers/task.controller.ts +158 -0
  44. package/src/http/express-augmentation.d.ts +20 -0
  45. package/src/http/middleware/cors.ts +41 -0
  46. package/src/http/middleware/error-handler.ts +50 -0
  47. package/src/http/middleware/request-id.ts +28 -0
  48. package/src/http/middleware/validate.ts +54 -0
  49. package/src/http/routes/v1/index.ts +39 -0
  50. package/src/http/routes/v1/plan.routes.ts +51 -0
  51. package/src/http/routes/v1/schemas.ts +175 -0
  52. package/src/http/routes/v1/sprint.routes.ts +49 -0
  53. package/src/http/routes/v1/task.routes.ts +64 -0
  54. package/src/index.ts +34 -0
  55. package/src/infrastructure/observability/audit-log.ts +34 -0
  56. package/src/infrastructure/observability/request-correlation.ts +20 -0
  57. package/src/infrastructure/repositories/drizzle/drizzle-plan.repository.ts +138 -0
  58. package/src/infrastructure/repositories/drizzle/drizzle-sprint.repository.ts +137 -0
  59. package/src/infrastructure/repositories/drizzle/drizzle-task.repository.ts +403 -0
  60. package/src/infrastructure/repositories/drizzle/index.ts +16 -0
  61. package/src/infrastructure/repositories/drizzle/row-mappers.ts +106 -0
  62. package/src/infrastructure/repositories/drizzle/sqlite-db.ts +13 -0
  63. package/src/infrastructure/repositories/repository-factory.ts +54 -0
  64. package/src/infrastructure/security/auth-context.ts +35 -0
  65. package/src/infrastructure/security/input-guard.ts +21 -0
  66. package/src/infrastructure/security/rate-limiter.ts +65 -0
  67. package/src/mcp/bootstrap-sprintdock-sqlite.ts +45 -0
  68. package/src/mcp/mcp-query-helpers.ts +89 -0
  69. package/src/mcp/mcp-text-formatters.ts +204 -0
  70. package/src/mcp/mcp-tool-error.ts +24 -0
  71. package/src/mcp/plugins/context-tools.plugin.ts +107 -0
  72. package/src/mcp/plugins/default-plugins.ts +23 -0
  73. package/src/mcp/plugins/index.ts +21 -0
  74. package/src/mcp/plugins/mcp-tool-kit.ts +90 -0
  75. package/src/mcp/plugins/plan-tools.plugin.ts +426 -0
  76. package/src/mcp/plugins/sprint-tools.plugin.ts +396 -0
  77. package/src/mcp/plugins/task-tools.plugin.ts +528 -0
  78. package/src/mcp/plugins/task-workflow.plugin.ts +275 -0
  79. package/src/mcp/plugins/types.ts +45 -0
  80. package/src/mcp/register-sprintdock-mcp-tools.ts +50 -0
  81. package/src/mcp/sprintdock-mcp-capabilities.ts +14 -0
  82. package/src/mcp/sprintdock-mcp-runtime.ts +119 -0
  83. package/src/mcp/tool-guard.ts +58 -0
  84. package/src/mcp/transports/http-app-factory.ts +31 -0
  85. package/src/mcp/transports/http-entry.ts +27 -0
  86. package/src/mcp/transports/stdio-entry.ts +17 -0
  87. package/tests/application/container.test.ts +36 -0
  88. package/tests/application/plan.service.test.ts +114 -0
  89. package/tests/application/sprint.service.test.ts +138 -0
  90. package/tests/application/task.service.test.ts +325 -0
  91. package/tests/db/test-db.test.ts +112 -0
  92. package/tests/domain/plan-domain.service.test.ts +44 -0
  93. package/tests/domain/sprint-domain.service.test.ts +38 -0
  94. package/tests/domain/task-domain.service.test.ts +105 -0
  95. package/tests/errors/backend-errors.test.ts +44 -0
  96. package/tests/helpers/test-db.ts +43 -0
  97. package/tests/http/error-handler.test.ts +37 -0
  98. package/tests/http/plan.routes.test.ts +128 -0
  99. package/tests/http/sprint.routes.test.ts +72 -0
  100. package/tests/http/task.routes.test.ts +130 -0
  101. package/tests/http/test-app.ts +17 -0
  102. package/tests/infrastructure/drizzle-plan.repository.test.ts +62 -0
  103. package/tests/infrastructure/drizzle-sprint.repository.test.ts +49 -0
  104. package/tests/infrastructure/drizzle-task.repository.test.ts +132 -0
  105. package/tests/mcp/mcp-text-formatters.test.ts +246 -0
  106. package/tests/mcp/register-sprintdock-mcp-tools.test.ts +207 -0
  107. 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
+ };