@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,275 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: MCP plugin — bulk task ops and dependency graph tools.
5
+ */
6
+ import { z } from "zod";
7
+ import { ensureTaskInPlan, getSprintForPlan, resolvePlanOrActive } from "../mcp-query-helpers.js";
8
+ import { shortId } from "../mcp-text-formatters.js";
9
+ import { toMcpToolError } from "../mcp-tool-error.js";
10
+ import { guardToolExecution, writeToolAudit } from "../tool-guard.js";
11
+ import type { SprintdockMcpToolPlugin } from "./types.js";
12
+
13
+ export const taskWorkflowPlugin: SprintdockMcpToolPlugin = {
14
+ id: "sprintdock/task-workflow",
15
+ register(server, ctx) {
16
+ const { deps, kit } = ctx;
17
+ const { jsonStructured, optionalPlanSlug, TASK_STATUS_SCHEMA, TASK_PRIORITY_SCHEMA } =
18
+ kit;
19
+ const { planService, sprintService, taskService } = deps.services;
20
+
21
+ server.registerTool(
22
+ "bulk_create_tasks",
23
+ {
24
+ description:
25
+ "Creates up to 50 tasks in one sprint in a single call. Pass sprintIdOrSlug (full UUID or sprint slug under the resolved plan). Each entry supports title, description, priority, assignee, and tags. Guarded mutation.",
26
+ inputSchema: z
27
+ .object({
28
+ sprintIdOrSlug: z.string().min(1),
29
+ tasks: z
30
+ .array(
31
+ z
32
+ .object({
33
+ title: z.string().min(1),
34
+ description: z.string().default(""),
35
+ priority: TASK_PRIORITY_SCHEMA.default("medium"),
36
+ assignee: z.string().nullable().optional(),
37
+ tags: z.array(z.string()).default([])
38
+ })
39
+ .strict()
40
+ )
41
+ .min(1)
42
+ .max(50),
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
+ "bulk_create_tasks"
54
+ );
55
+ const plan = await resolvePlanOrActive(planService, input.planSlug);
56
+ const sprint = await getSprintForPlan(
57
+ sprintService,
58
+ plan,
59
+ input.sprintIdOrSlug
60
+ );
61
+ const created = [];
62
+ for (const t of input.tasks) {
63
+ const task = await taskService.createTask(
64
+ {
65
+ title: t.title,
66
+ description: t.description || null,
67
+ priority: t.priority ?? "medium",
68
+ sprintId: sprint.id,
69
+ assignee: t.assignee ?? null,
70
+ tags: t.tags ?? []
71
+ },
72
+ input.planSlug
73
+ );
74
+ created.push(task);
75
+ }
76
+ writeToolAudit(
77
+ deps.auditLog,
78
+ "bulk_create_tasks",
79
+ guard.principalId,
80
+ guard.correlationId,
81
+ sprint.id,
82
+ { count: created.length }
83
+ );
84
+ const lines = created.map(
85
+ (x) => `- '${x.title}' (id: ${shortId(x.id)})`
86
+ );
87
+ return {
88
+ content: [
89
+ {
90
+ type: "text",
91
+ text: `Created ${created.length} task(s):\n${lines.join("\n")}`
92
+ }
93
+ ],
94
+ structuredContent: jsonStructured({ tasks: created })
95
+ };
96
+ } catch (e) {
97
+ return toMcpToolError(e);
98
+ }
99
+ }
100
+ );
101
+
102
+ server.registerTool(
103
+ "bulk_update_task_status",
104
+ {
105
+ description:
106
+ "Updates status on up to 50 tasks in one call. Each pair must include taskId and target status. Guarded mutation.",
107
+ inputSchema: z
108
+ .object({
109
+ updates: z
110
+ .array(
111
+ z
112
+ .object({
113
+ taskId: z.string().min(1),
114
+ status: TASK_STATUS_SCHEMA
115
+ })
116
+ .strict()
117
+ )
118
+ .min(1)
119
+ .max(50),
120
+ planSlug: optionalPlanSlug
121
+ })
122
+ .strict()
123
+ },
124
+ async (input) => {
125
+ try {
126
+ const guard = await guardToolExecution(
127
+ deps.authContextResolver,
128
+ deps.requestCorrelation,
129
+ deps.rateLimiter,
130
+ "bulk_update_task_status"
131
+ );
132
+ const done = [];
133
+ for (const u of input.updates) {
134
+ if (input.planSlug !== undefined && input.planSlug !== "") {
135
+ const plan = await planService.resolvePlan(input.planSlug);
136
+ await ensureTaskInPlan(
137
+ taskService,
138
+ sprintService,
139
+ plan,
140
+ u.taskId
141
+ );
142
+ }
143
+ const task = await taskService.updateTaskStatus(
144
+ u.taskId,
145
+ u.status
146
+ );
147
+ done.push(task);
148
+ }
149
+ writeToolAudit(
150
+ deps.auditLog,
151
+ "bulk_update_task_status",
152
+ guard.principalId,
153
+ guard.correlationId,
154
+ "bulk",
155
+ { count: done.length }
156
+ );
157
+ const lines = done.map(
158
+ (x) => `- '${x.title}' -> ${x.status} (id: ${shortId(x.id)})`
159
+ );
160
+ return {
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: `Updated ${done.length} task(s):\n${lines.join("\n")}`
165
+ }
166
+ ],
167
+ structuredContent: jsonStructured({ tasks: done })
168
+ };
169
+ } catch (e) {
170
+ return toMcpToolError(e);
171
+ }
172
+ }
173
+ );
174
+
175
+ server.registerTool(
176
+ "get_task_dependencies",
177
+ {
178
+ description:
179
+ "Returns tasks this task depends on (prerequisites) and tasks that depend on it (dependents), scoped to the plan when planSlug is set.",
180
+ inputSchema: z
181
+ .object({
182
+ taskId: z.string().min(1),
183
+ planSlug: optionalPlanSlug
184
+ })
185
+ .strict()
186
+ },
187
+ async (input) => {
188
+ try {
189
+ const { dependsOn, dependedOnBy } =
190
+ await taskService.getTaskDependencyInfo(
191
+ input.taskId,
192
+ input.planSlug
193
+ );
194
+ const task = await taskService.getTask(input.taskId);
195
+ const preLines = dependsOn.map(
196
+ (t) => `- '${t.title}' (id: ${shortId(t.id)})`
197
+ );
198
+ const postLines = dependedOnBy.map(
199
+ (t) => `- '${t.title}' (id: ${shortId(t.id)})`
200
+ );
201
+ const text = [
202
+ `Dependencies for '${task.title}' (id: ${shortId(task.id)}):`,
203
+ "Depends on:",
204
+ preLines.length ? preLines.join("\n") : "(none)",
205
+ "Depended on by:",
206
+ postLines.length ? postLines.join("\n") : "(none)"
207
+ ].join("\n");
208
+ return {
209
+ content: [{ type: "text", text }],
210
+ structuredContent: jsonStructured({
211
+ task,
212
+ dependsOn,
213
+ dependedOnBy
214
+ })
215
+ };
216
+ } catch (e) {
217
+ return toMcpToolError(e);
218
+ }
219
+ }
220
+ );
221
+
222
+ server.registerTool(
223
+ "update_task_dependencies",
224
+ {
225
+ description:
226
+ "Replaces the full prerequisite list for a task (depends-on edges). Validates acyclic graph within the plan. Guarded mutation.",
227
+ inputSchema: z
228
+ .object({
229
+ taskId: z.string().min(1),
230
+ dependsOnTaskIds: z.array(z.string()),
231
+ planSlug: optionalPlanSlug
232
+ })
233
+ .strict()
234
+ },
235
+ async (input) => {
236
+ try {
237
+ const guard = await guardToolExecution(
238
+ deps.authContextResolver,
239
+ deps.requestCorrelation,
240
+ deps.rateLimiter,
241
+ "update_task_dependencies"
242
+ );
243
+ await taskService.setTaskDependencies(
244
+ input.taskId,
245
+ input.dependsOnTaskIds,
246
+ input.planSlug
247
+ );
248
+ const task = await taskService.getTask(input.taskId);
249
+ writeToolAudit(
250
+ deps.auditLog,
251
+ "update_task_dependencies",
252
+ guard.principalId,
253
+ guard.correlationId,
254
+ input.taskId,
255
+ { count: input.dependsOnTaskIds.length }
256
+ );
257
+ return {
258
+ content: [
259
+ {
260
+ type: "text",
261
+ text: `Task '${task.title}' dependencies set (${input.dependsOnTaskIds.length} edge(s)). (id: ${shortId(task.id)})`
262
+ }
263
+ ],
264
+ structuredContent: jsonStructured({
265
+ taskId: input.taskId,
266
+ dependsOnTaskIds: input.dependsOnTaskIds
267
+ })
268
+ };
269
+ } catch (e) {
270
+ return toMcpToolError(e);
271
+ }
272
+ }
273
+ );
274
+ }
275
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Plugin contracts for composing Sprintdock MCP tool registration.
5
+ */
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import type { ServiceSet } from "../../application/container.js";
8
+ import type { AuditLog } from "../../infrastructure/observability/audit-log.js";
9
+ import type { RequestCorrelation } from "../../infrastructure/observability/request-correlation.js";
10
+ import type { AuthContextResolver } from "../../infrastructure/security/auth-context.js";
11
+ import type { BackendRateLimiter } from "../../infrastructure/security/rate-limiter.js";
12
+ import type { SprintdockMcpToolKit } from "./mcp-tool-kit.js";
13
+
14
+ /** Injected services and security hooks for MCP tool handlers. */
15
+ export interface SprintdockMcpToolDependencies {
16
+ services: ServiceSet;
17
+ auditLog: AuditLog;
18
+ rateLimiter: BackendRateLimiter;
19
+ requestCorrelation: RequestCorrelation;
20
+ authContextResolver: AuthContextResolver;
21
+ }
22
+
23
+ /** Context passed to each plugin's `register` method. */
24
+ export interface SprintdockMcpPluginContext {
25
+ deps: SprintdockMcpToolDependencies;
26
+ kit: SprintdockMcpToolKit;
27
+ }
28
+
29
+ /**
30
+ * A plugin registers one or more MCP tools. Implement `id` for logging;
31
+ * use `register` to call `server.registerTool` like the built-in plugins.
32
+ */
33
+ export interface SprintdockMcpToolPlugin {
34
+ readonly id: string;
35
+ register(server: McpServer, ctx: SprintdockMcpPluginContext): void;
36
+ }
37
+
38
+ /** Optional configuration when registering tools on the MCP server. */
39
+ export interface RegisterSprintdockMcpToolsOptions {
40
+ /**
41
+ * Plugins to load, in order. Defaults to the full built-in Sprintdock set.
42
+ * Append custom plugins to add tools; pass a subset to expose only part of the API.
43
+ */
44
+ plugins?: SprintdockMcpToolPlugin[];
45
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Composes Sprintdock MCP tools from plugins (default set + optional extensions).
5
+ */
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import {
8
+ createSprintdockMcpToolKit,
9
+ defaultSprintdockMcpPlugins
10
+ } from "./plugins/index.js";
11
+ import type {
12
+ RegisterSprintdockMcpToolsOptions,
13
+ SprintdockMcpToolDependencies
14
+ } from "./plugins/types.js";
15
+
16
+ export type {
17
+ RegisterSprintdockMcpToolsOptions,
18
+ SprintdockMcpPluginContext,
19
+ SprintdockMcpToolDependencies,
20
+ SprintdockMcpToolKit,
21
+ SprintdockMcpToolPlugin
22
+ } from "./plugins/index.js";
23
+
24
+ export {
25
+ createSprintdockMcpToolKit,
26
+ defaultSprintdockMcpPlugins
27
+ } from "./plugins/index.js";
28
+
29
+ /**
30
+ * Registers MCP tools by running each plugin in order against the server.
31
+ *
32
+ * @example Add a custom tool plugin after the defaults:
33
+ * ```ts
34
+ * registerSprintdockMcpTools(server, deps, {
35
+ * plugins: [...defaultSprintdockMcpPlugins, myCustomPlugin]
36
+ * });
37
+ * ```
38
+ */
39
+ export function registerSprintdockMcpTools(
40
+ server: McpServer,
41
+ d: SprintdockMcpToolDependencies,
42
+ options?: RegisterSprintdockMcpToolsOptions
43
+ ): void {
44
+ const kit = createSprintdockMcpToolKit();
45
+ const ctx = { deps: d, kit };
46
+ const plugins = options?.plugins ?? [...defaultSprintdockMcpPlugins];
47
+ for (const plugin of plugins) {
48
+ plugin.register(server, ctx);
49
+ }
50
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: MCP capability flags for Sprintdock SQLite runtime.
5
+ */
6
+ import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
7
+
8
+ /** Declares tool support for the v2 MCP server. */
9
+ export const SPRINTDOCK_MCP_CAPABILITIES: ServerCapabilities = {
10
+ tools: {
11
+ listChanged: true
12
+ },
13
+ logging: {}
14
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: MCP runtime: SQLite bootstrap, tool registration, stdio or HTTP transport.
5
+ */
6
+ import { getEnv } from "@sprintdock/env-manager";
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { resolveWorkspaceRoot } from "@sprintdock/shared";
9
+ import { ConsoleAuditLog } from "../infrastructure/observability/audit-log.js";
10
+ import { RequestCorrelation } from "../infrastructure/observability/request-correlation.js";
11
+ import { LocalAuthContextResolver } from "../infrastructure/security/auth-context.js";
12
+ import { InMemoryBackendRateLimiter } from "../infrastructure/security/rate-limiter.js";
13
+ import { bootstrapSprintdockSqlite } from "./bootstrap-sprintdock-sqlite.js";
14
+ import { registerSprintdockMcpTools, type SprintdockMcpToolDependencies } from "./register-sprintdock-mcp-tools.js";
15
+ import { SPRINTDOCK_MCP_CAPABILITIES } from "./sprintdock-mcp-capabilities.js";
16
+ import { runHttpTransport } from "./transports/http-entry.js";
17
+ import { runStdioTransport } from "./transports/stdio-entry.js";
18
+
19
+ export type SprintdockMcpTransportMode = "stdio" | "http";
20
+
21
+ export interface SprintdockMcpRuntimeOptions {
22
+ transport?: SprintdockMcpTransportMode;
23
+ httpHost?: string;
24
+ httpPort?: number;
25
+ }
26
+
27
+ /**
28
+ * Runs Sprintdock MCP against the workspace SQLite database.
29
+ */
30
+ export class SprintdockMcpRuntime {
31
+ private readonly options: Required<
32
+ Pick<SprintdockMcpRuntimeOptions, "transport">
33
+ > &
34
+ SprintdockMcpRuntimeOptions;
35
+
36
+ private readonly workspaceRoot: string;
37
+ private toolDeps: SprintdockMcpToolDependencies | null = null;
38
+
39
+ public constructor(options?: SprintdockMcpRuntimeOptions) {
40
+ this.workspaceRoot = resolveWorkspaceRoot();
41
+ const transportEnv = getEnv("SPRINTDOCK_MCP_TRANSPORT", "stdio", {
42
+ projectRoot: this.workspaceRoot
43
+ }) as SprintdockMcpTransportMode;
44
+ this.options = {
45
+ transport: options?.transport ?? transportEnv,
46
+ httpHost: options?.httpHost,
47
+ httpPort: options?.httpPort
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Boots SQLite, registers tools, and starts the selected transport.
53
+ *
54
+ * @returns Process-style exit code (0 on success).
55
+ */
56
+ public async run(): Promise<number> {
57
+ const { services, dbPath } = await bootstrapSprintdockSqlite();
58
+ process.stderr.write(
59
+ `[sprintdock:backend:mcp] sqlite database: ${dbPath}\n`
60
+ );
61
+ this.toolDeps = {
62
+ services,
63
+ auditLog: new ConsoleAuditLog(),
64
+ rateLimiter: new InMemoryBackendRateLimiter(),
65
+ requestCorrelation: new RequestCorrelation(),
66
+ authContextResolver: new LocalAuthContextResolver()
67
+ };
68
+
69
+ if (this.options.transport === "http") {
70
+ const host =
71
+ this.options.httpHost ??
72
+ getEnv("SPRINTDOCK_MCP_HTTP_HOST", "127.0.0.1", {
73
+ projectRoot: this.workspaceRoot
74
+ });
75
+ const portRaw =
76
+ this.options.httpPort ??
77
+ Number(
78
+ getEnv("SPRINTDOCK_MCP_HTTP_PORT", "3030", {
79
+ projectRoot: this.workspaceRoot
80
+ })
81
+ );
82
+ if (!Number.isFinite(portRaw)) {
83
+ throw new Error("SPRINTDOCK_MCP_HTTP_PORT must be a valid number.");
84
+ }
85
+ return runHttpTransport(() => this.createMcpServer(), host, portRaw);
86
+ }
87
+
88
+ return runStdioTransport(this.createMcpServer());
89
+ }
90
+
91
+ private createMcpServer(): McpServer {
92
+ if (!this.toolDeps) {
93
+ throw new Error("SprintdockMcpRuntime tool dependencies not initialized");
94
+ }
95
+ const server = new McpServer(
96
+ {
97
+ name: "sprintdock-backend",
98
+ version: "0.2.0"
99
+ },
100
+ {
101
+ capabilities: SPRINTDOCK_MCP_CAPABILITIES,
102
+ instructions:
103
+ "Sprintdock MCP (SQLite): data lives in .sprintdock/sprintdock.db. Use list_plans / create_plan / set_active_plan. Optional planSlug on sprint/task tools scopes to that plan. Plan/sprint/task tools include update_plan_markdown and update_sprint_markdown for long-form notes."
104
+ }
105
+ );
106
+ registerSprintdockMcpTools(server, this.toolDeps);
107
+ return server;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Convenience entrypoint for `apps/server` and scripts.
113
+ */
114
+ export async function runSprintdockMcpServer(
115
+ options?: SprintdockMcpRuntimeOptions
116
+ ): Promise<number> {
117
+ const runtime = new SprintdockMcpRuntime(options);
118
+ return runtime.run();
119
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: MCP tool authorization guard and audit helpers (stdio transport).
5
+ */
6
+ import type { AuditLog } from "../infrastructure/observability/audit-log.js";
7
+ import type { RequestCorrelation } from "../infrastructure/observability/request-correlation.js";
8
+ import type { AuthContextResolver } from "../infrastructure/security/auth-context.js";
9
+ import type { BackendRateLimiter } from "../infrastructure/security/rate-limiter.js";
10
+
11
+ export interface ToolGuardResult {
12
+ principalId: string;
13
+ correlationId: string;
14
+ }
15
+
16
+ /**
17
+ * Resolves auth context, creates correlation id, and enforces per-principal rate limits.
18
+ */
19
+ export async function guardToolExecution(
20
+ authContextResolver: AuthContextResolver,
21
+ requestCorrelation: RequestCorrelation,
22
+ rateLimiter: BackendRateLimiter,
23
+ toolName: string
24
+ ): Promise<ToolGuardResult> {
25
+ const authContext = await authContextResolver.resolve("stdio");
26
+ const correlationId = requestCorrelation.create();
27
+ const rateLimit = rateLimiter.check(`${authContext.principalId}:${toolName}`);
28
+ if (!rateLimit.allowed) {
29
+ throw new Error(
30
+ `Rate limit exceeded for tool '${toolName}'. Retry after ${rateLimit.retryAfterSeconds}s.`
31
+ );
32
+ }
33
+ return {
34
+ principalId: authContext.principalId,
35
+ correlationId
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Writes an audit record for a tool invocation.
41
+ */
42
+ export function writeToolAudit(
43
+ auditLog: AuditLog,
44
+ action: string,
45
+ principalId: string,
46
+ correlationId: string,
47
+ resourceId?: string,
48
+ metadata?: Record<string, unknown>
49
+ ): void {
50
+ auditLog.write({
51
+ action,
52
+ principalId,
53
+ transport: "stdio",
54
+ resourceId,
55
+ correlationId,
56
+ metadata
57
+ });
58
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Express app with streamable HTTP MCP endpoint at `/mcp`.
5
+ */
6
+ import { randomUUID } from "node:crypto";
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
9
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
+ import type { Request, Response } from "express";
11
+
12
+ /**
13
+ * Builds an Express app that serves MCP over streamable HTTP at `/mcp`.
14
+ */
15
+ export function createSprintdockMcpHttpApp(
16
+ serverFactory: () => McpServer,
17
+ host: string
18
+ ) {
19
+ const app = createMcpExpressApp({ host });
20
+
21
+ app.use("/mcp", async (request: Request, response: Response) => {
22
+ const server = serverFactory();
23
+ const transport = new StreamableHTTPServerTransport({
24
+ sessionIdGenerator: () => randomUUID()
25
+ });
26
+ await server.connect(transport);
27
+ await transport.handleRequest(request, response);
28
+ });
29
+
30
+ return app;
31
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Streamable HTTP MCP transport bootstrap.
5
+ */
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { createSprintdockMcpHttpApp } from "./http-app-factory.js";
8
+
9
+ /**
10
+ * Listens for MCP HTTP traffic on the given host/port (`/mcp`).
11
+ */
12
+ export async function runHttpTransport(
13
+ serverFactory: () => McpServer,
14
+ host: string,
15
+ port: number
16
+ ): Promise<number> {
17
+ const app = createSprintdockMcpHttpApp(serverFactory, host);
18
+ await new Promise<void>((resolve) => {
19
+ app.listen(port, host, () => {
20
+ process.stderr.write(
21
+ `[sprintdock:backend:mcp] streamable-http listening at http://${host}:${port}/mcp\n`
22
+ );
23
+ resolve();
24
+ });
25
+ });
26
+ return 0;
27
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Stdio MCP transport bootstrap.
5
+ */
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+
9
+ /**
10
+ * Connects the MCP server to stdio and blocks for the process lifetime.
11
+ */
12
+ export async function runStdioTransport(server: McpServer): Promise<number> {
13
+ const transport = new StdioServerTransport();
14
+ await server.connect(transport);
15
+ process.stderr.write("[sprintdock:backend:mcp] stdio transport connected\n");
16
+ return 0;
17
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Tests for createApplicationServices wiring.
5
+ */
6
+ import assert from "node:assert/strict";
7
+ import { test } from "node:test";
8
+ import { createApplicationServices } from "../../src/application/container.js";
9
+ import { createRepositories } from "../../src/infrastructure/repositories/repository-factory.js";
10
+ import { createTestDb } from "../helpers/test-db.js";
11
+
12
+ test("createApplicationServices returns working plan, sprint, and task services", async () => {
13
+ const db = await createTestDb();
14
+ const repos = createRepositories("sqlite", { sqlite: { db } });
15
+ const { planService, sprintService, taskService } =
16
+ createApplicationServices(repos);
17
+
18
+ const plan = await planService.createPlan({ slug: "c", title: "C" });
19
+ const sp = await sprintService.createSprint(plan.slug, {
20
+ slug: "sp",
21
+ planId: plan.id,
22
+ name: "S",
23
+ goal: "G"
24
+ });
25
+ const task = await taskService.createTask(
26
+ {
27
+ sprintId: sp.id,
28
+ title: "T",
29
+ priority: "low"
30
+ },
31
+ plan.slug
32
+ );
33
+ assert.equal(task.sprintId, sp.id);
34
+ const listed = await taskService.listTasks(undefined, plan.slug);
35
+ assert.equal(listed.length, 1);
36
+ });