@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,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
|
+
});
|