@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,54 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Factory wiring repository ports to concrete storage adapters.
5
+ */
6
+ import type { PlanRepository } from "../../domain/repositories/plan.repository";
7
+ import type { SprintRepository } from "../../domain/repositories/sprint.repository";
8
+ import type { TaskRepository } from "../../domain/repositories/task.repository";
9
+ import {
10
+ DrizzlePlanRepository,
11
+ DrizzleSprintRepository,
12
+ DrizzleTaskRepository,
13
+ type DrizzleSqliteDb
14
+ } from "./drizzle/index";
15
+
16
+ /** Supported storage backends for repository construction. */
17
+ export type StorageAdapterType = "sqlite";
18
+
19
+ /** Per-adapter connection options. */
20
+ export interface StorageConfig {
21
+ sqlite?: { db: DrizzleSqliteDb };
22
+ }
23
+
24
+ /** Wired repository set for application services. */
25
+ export interface RepositorySet {
26
+ plans: PlanRepository;
27
+ sprints: SprintRepository;
28
+ tasks: TaskRepository;
29
+ }
30
+
31
+ /**
32
+ * Instantiates repository implementations for the selected adapter.
33
+ *
34
+ * @param adapter Storage kind (currently only sqlite).
35
+ * @param config Connection options for the adapter.
36
+ * @returns Repository ports backed by the configured database.
37
+ */
38
+ export function createRepositories(
39
+ adapter: StorageAdapterType,
40
+ config: StorageConfig
41
+ ): RepositorySet {
42
+ if (adapter !== "sqlite") {
43
+ throw new Error(`Unsupported storage adapter: ${adapter}`);
44
+ }
45
+ const db = config.sqlite?.db;
46
+ if (!db) {
47
+ throw new Error('createRepositories("sqlite") requires config.sqlite.db');
48
+ }
49
+ return {
50
+ plans: new DrizzlePlanRepository(db),
51
+ sprints: new DrizzleSprintRepository(db),
52
+ tasks: new DrizzleTaskRepository(db)
53
+ };
54
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Authentication context contracts for MCP request authorization.
5
+ */
6
+
7
+ /**
8
+ * Principal context resolved for each request.
9
+ */
10
+ export interface AuthContext {
11
+ principalId: string;
12
+ scopes: readonly string[];
13
+ transport: "stdio" | "http";
14
+ }
15
+
16
+ /**
17
+ * Resolver interface for obtaining request principal details.
18
+ */
19
+ export interface AuthContextResolver {
20
+ resolve(transport: "stdio" | "http"): Promise<AuthContext>;
21
+ }
22
+
23
+ /**
24
+ * Local development resolver for no-auth environments.
25
+ */
26
+ export class LocalAuthContextResolver implements AuthContextResolver {
27
+ /** @inheritdoc */
28
+ public async resolve(transport: "stdio" | "http"): Promise<AuthContext> {
29
+ return {
30
+ principalId: "local-dev-principal",
31
+ scopes: ["sprintdock:read", "sprintdock:write"],
32
+ transport
33
+ };
34
+ }
35
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Strict schema parsing helpers for adapter and HTTP inputs.
5
+ */
6
+ import { z, type ZodTypeAny } from "zod";
7
+
8
+ /**
9
+ * Validates and parses unknown values using strict schema constraints.
10
+ *
11
+ * @param schema Zod schema used for validation.
12
+ * @param input Unknown payload value.
13
+ * @returns Parsed payload.
14
+ * @throws ZodError when validation fails.
15
+ */
16
+ export function parseStrict<T extends ZodTypeAny>(
17
+ schema: T,
18
+ input: unknown
19
+ ): z.infer<T> {
20
+ return schema.parse(input);
21
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Lightweight per-key rate limiter for MCP tool invocations.
5
+ */
6
+
7
+ /**
8
+ * Rate limiter result for caller decisions.
9
+ */
10
+ export interface RateLimitResult {
11
+ allowed: boolean;
12
+ retryAfterSeconds: number;
13
+ }
14
+
15
+ /**
16
+ * Contract for backend tool-call throttling.
17
+ */
18
+ export interface BackendRateLimiter {
19
+ check(key: string): RateLimitResult;
20
+ }
21
+
22
+ /**
23
+ * In-memory fixed-window limiter for local runtime safety.
24
+ */
25
+ export class InMemoryBackendRateLimiter implements BackendRateLimiter {
26
+ private readonly maxRequests: number;
27
+ private readonly windowMs: number;
28
+ private readonly requestWindows: Map<string, number[]>;
29
+
30
+ /**
31
+ * @param maxRequests Maximum allowed requests per window.
32
+ * @param windowMs Window size in milliseconds.
33
+ */
34
+ public constructor(maxRequests = 120, windowMs = 60_000) {
35
+ this.maxRequests = maxRequests;
36
+ this.windowMs = windowMs;
37
+ this.requestWindows = new Map<string, number[]>();
38
+ }
39
+
40
+ /** @inheritdoc */
41
+ public check(key: string): RateLimitResult {
42
+ const now = Date.now();
43
+ const threshold = now - this.windowMs;
44
+ const previous = this.requestWindows.get(key) ?? [];
45
+ const currentWindow = previous.filter((timestamp) => timestamp > threshold);
46
+ if (currentWindow.length >= this.maxRequests) {
47
+ const oldestRequestTimestamp = currentWindow[0] ?? now;
48
+ const retryAfterSeconds = Math.max(
49
+ 1,
50
+ Math.ceil((oldestRequestTimestamp + this.windowMs - now) / 1000)
51
+ );
52
+ this.requestWindows.set(key, currentWindow);
53
+ return {
54
+ allowed: false,
55
+ retryAfterSeconds
56
+ };
57
+ }
58
+ currentWindow.push(now);
59
+ this.requestWindows.set(key, currentWindow);
60
+ return {
61
+ allowed: true,
62
+ retryAfterSeconds: 0
63
+ };
64
+ }
65
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Ensures `.sprintdock/`, opens SQLite, migrates, and wires application services.
5
+ */
6
+ import { mkdirSync } from "node:fs";
7
+ import { dirname, isAbsolute, join } from "node:path";
8
+ import {
9
+ ensureSprintdockDirectory,
10
+ resolveSprintdockDbPath,
11
+ resolveWorkspaceRoot
12
+ } from "@sprintdock/shared";
13
+ import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
14
+ import * as schema from "../db/schema/index.js";
15
+ import { createApplicationServices, type ServiceSet } from "../application/container.js";
16
+ import { createSqliteConnection } from "../db/connection.js";
17
+ import { runMigrations } from "../db/migrator.js";
18
+ import { createRepositories } from "../infrastructure/repositories/repository-factory.js";
19
+
20
+ export interface SprintdockSqliteBootstrapResult {
21
+ db: BetterSQLite3Database<typeof schema>;
22
+ services: ServiceSet;
23
+ dbPath: string;
24
+ }
25
+
26
+ /**
27
+ * Resolves DB path (`SPRINTDOCK_DB_PATH` or `.sprintdock/sprintdock.db`), migrates, and returns services.
28
+ */
29
+ export async function bootstrapSprintdockSqlite(): Promise<SprintdockSqliteBootstrapResult> {
30
+ const workspaceRoot = resolveWorkspaceRoot();
31
+ await ensureSprintdockDirectory({ workspaceRoot });
32
+ const envPath = process.env.SPRINTDOCK_DB_PATH?.trim();
33
+ const dbPath =
34
+ envPath && envPath.length > 0
35
+ ? isAbsolute(envPath)
36
+ ? envPath
37
+ : join(workspaceRoot, envPath)
38
+ : resolveSprintdockDbPath({ workspaceRoot });
39
+ mkdirSync(dirname(dbPath), { recursive: true });
40
+ const db = createSqliteConnection(dbPath);
41
+ runMigrations(db);
42
+ const repos = createRepositories("sqlite", { sqlite: { db } });
43
+ const services = createApplicationServices(repos);
44
+ return { db, services, dbPath };
45
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Shared helpers for MCP tools (plan scoping, aggregates).
5
+ */
6
+ import { ValidationError } from "../errors/backend-errors.js";
7
+ import type { Plan } from "../domain/entities/plan.entity";
8
+ import type { Sprint } from "../domain/entities/sprint.entity";
9
+ import type { Task, TaskStatus } from "../domain/entities/task.entity";
10
+ import type { PlanService } from "../application/plan.service.js";
11
+ import type { SprintService } from "../application/sprint.service.js";
12
+ import type { TaskService } from "../application/task.service.js";
13
+
14
+ const UUID_RE =
15
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
16
+
17
+ export function looksLikeUuid(value: string): boolean {
18
+ return UUID_RE.test(value);
19
+ }
20
+
21
+ /**
22
+ * Resolves an explicit plan slug/id or falls back to the active plan.
23
+ */
24
+ export async function resolvePlanOrActive(
25
+ planService: PlanService,
26
+ planSlug?: string
27
+ ): Promise<Plan> {
28
+ if (planSlug !== undefined && planSlug !== "") {
29
+ return planService.resolvePlan(planSlug);
30
+ }
31
+ return planService.getActivePlan();
32
+ }
33
+
34
+ export function countByTaskStatus(
35
+ tasks: Task[]
36
+ ): Record<TaskStatus, number> {
37
+ const base: Record<TaskStatus, number> = {
38
+ todo: 0,
39
+ in_progress: 0,
40
+ blocked: 0,
41
+ done: 0
42
+ };
43
+ for (const t of tasks) {
44
+ base[t.status] += 1;
45
+ }
46
+ return base;
47
+ }
48
+
49
+ /**
50
+ * Loads a sprint by UUID or by slug under the given plan.
51
+ */
52
+ export async function getSprintForPlan(
53
+ sprintService: SprintService,
54
+ plan: Plan,
55
+ sprintIdOrSlug: string
56
+ ): Promise<Sprint> {
57
+ if (looksLikeUuid(sprintIdOrSlug)) {
58
+ const sp = await sprintService.getSprint(sprintIdOrSlug);
59
+ if (sp.planId !== plan.id) {
60
+ throw new ValidationError("Sprint does not belong to the resolved plan");
61
+ }
62
+ return sp;
63
+ }
64
+ return sprintService.getSprintBySlug(plan.id, sprintIdOrSlug);
65
+ }
66
+
67
+ export async function ensureSprintInPlan(
68
+ sprintService: SprintService,
69
+ plan: Plan,
70
+ sprintId: string
71
+ ): Promise<Sprint> {
72
+ const sp = await sprintService.getSprint(sprintId);
73
+ if (sp.planId !== plan.id) {
74
+ throw new ValidationError("Sprint does not belong to the resolved plan");
75
+ }
76
+ return sp;
77
+ }
78
+
79
+ export async function ensureTaskInPlan(
80
+ taskService: TaskService,
81
+ sprintService: SprintService,
82
+ plan: Plan,
83
+ taskId: string
84
+ ): Promise<Task> {
85
+ const task = await taskService.getTask(taskId);
86
+ await ensureSprintInPlan(sprintService, plan, task.sprintId);
87
+ return task;
88
+ }
89
+
@@ -0,0 +1,204 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Human-readable MCP tool text for AI agents (lists, summaries, sprint detail).
5
+ */
6
+ import type { Plan } from "../domain/entities/plan.entity";
7
+ import type { Sprint } from "../domain/entities/sprint.entity";
8
+ import type { Task, TaskStatus } from "../domain/entities/task.entity";
9
+
10
+ /** Sprint row plus per-status task counts (for list_sprints / summaries). */
11
+ export type SprintWithTaskCounts = Sprint & {
12
+ taskCounts: Record<TaskStatus, number>;
13
+ };
14
+
15
+ const STATUS_ORDER: TaskStatus[] = [
16
+ "todo",
17
+ "in_progress",
18
+ "blocked",
19
+ "done"
20
+ ];
21
+
22
+ /**
23
+ * Short stable task/sprint reference: first 8 hex digits of the UUID (no dashes).
24
+ */
25
+ export function shortId(uuid: string): string {
26
+ return uuid.replace(/-/g, "").slice(0, 8);
27
+ }
28
+
29
+ function formatTaskCounts(counts: Record<TaskStatus, number>): string {
30
+ const parts: string[] = [];
31
+ for (const s of STATUS_ORDER) {
32
+ const n = counts[s];
33
+ if (n > 0) {
34
+ parts.push(`${n} ${s}`);
35
+ }
36
+ }
37
+ return parts.join(", ");
38
+ }
39
+
40
+ function formatTaskLine(task: Task, indent = ""): string {
41
+ const assignee =
42
+ task.assignee !== null && task.assignee !== ""
43
+ ? `, assignee: ${task.assignee}`
44
+ : "";
45
+ return `${indent}- [${task.status}] "${task.title}" (priority: ${task.priority}${assignee}) id:${shortId(task.id)}`;
46
+ }
47
+
48
+ /**
49
+ * Formats the plan directory list for `list_plans`.
50
+ */
51
+ export function formatPlanList(plans: Plan[], activePlanId: string | null): string {
52
+ const header = `Plans (${plans.length}):`;
53
+ if (plans.length === 0) {
54
+ return header;
55
+ }
56
+ const lines = plans.map((p) => {
57
+ const activeSuffix = p.isActive || p.id === activePlanId ? " (active plan)" : "";
58
+ return `- ${p.slug} [${p.status}] "${p.title}"${activeSuffix}`;
59
+ });
60
+ return `${header}\n${lines.join("\n")}`;
61
+ }
62
+
63
+ /**
64
+ * Formats sprints with per-sprint task counts for `list_sprints`.
65
+ */
66
+ export function formatSprintList(
67
+ planSlug: string,
68
+ sprints: SprintWithTaskCounts[]
69
+ ): string {
70
+ const header = `Sprints for '${planSlug}' (${sprints.length}):`;
71
+ if (sprints.length === 0) {
72
+ return header;
73
+ }
74
+ const lines = sprints.map((s) => {
75
+ const tc = formatTaskCounts(s.taskCounts);
76
+ const countsPart = tc.length > 0 ? ` | ${tc}` : "";
77
+ return `- ${s.slug} [${s.status}] "${s.name}"${countsPart} | sprintId:${s.id}`;
78
+ });
79
+ return `${header}\n${lines.join("\n")}`;
80
+ }
81
+
82
+ /**
83
+ * Formats a flat task list for `list_tasks` and nested views.
84
+ */
85
+ export function formatTaskList(tasks: Task[], headerLabel = "Tasks"): string {
86
+ const header = `${headerLabel} (${tasks.length}):`;
87
+ if (tasks.length === 0) {
88
+ return header;
89
+ }
90
+ const lines = tasks.map((t) => formatTaskLine(t));
91
+ return `${header}\n${lines.join("\n")}`;
92
+ }
93
+
94
+ export type PlanSummarySprintRow = {
95
+ sprintSlug: string;
96
+ sprintId: string;
97
+ name: string;
98
+ status: Sprint["status"];
99
+ taskCounts: Record<TaskStatus, number>;
100
+ };
101
+
102
+ /**
103
+ * Rich plan overview for `get_plan_summary`.
104
+ */
105
+ export function formatPlanSummary(
106
+ plan: Plan,
107
+ sprintsOut: PlanSummarySprintRow[]
108
+ ): string {
109
+ let totalTasks = 0;
110
+ for (const s of sprintsOut) {
111
+ totalTasks += Object.values(s.taskCounts).reduce((a, b) => a + b, 0);
112
+ }
113
+ const sprintWord = sprintsOut.length === 1 ? "sprint" : "sprints";
114
+ const taskWord = totalTasks === 1 ? "task" : "tasks";
115
+ const head = `Plan '${plan.slug}' ("${plan.title}") -- ${sprintsOut.length} ${sprintWord}, ${totalTasks} ${taskWord} total`;
116
+ if (sprintsOut.length === 0) {
117
+ return head;
118
+ }
119
+ const breakdown = sprintsOut.map((s) => {
120
+ const tc = formatTaskCounts(s.taskCounts);
121
+ const countsPart = tc.length > 0 ? `: ${tc}` : ":";
122
+ return `- ${s.sprintSlug} [${s.status}]${countsPart} (id: ${shortId(s.sprintId)})`;
123
+ });
124
+ return `${head}\n\nSprint breakdown:\n${breakdown.join("\n")}`;
125
+ }
126
+
127
+ export type ExecutionSprintBlock = {
128
+ sprintSlug: string;
129
+ sprint: Sprint;
130
+ tasks: Task[];
131
+ taskCounts: Record<TaskStatus, number>;
132
+ };
133
+
134
+ /**
135
+ * Primary agent context view for `get_execution_context`.
136
+ */
137
+ export function formatExecutionContext(
138
+ planSlug: string,
139
+ activeSprints: ExecutionSprintBlock[]
140
+ ): string {
141
+ const head = `Execution context for '${planSlug}':\nActive sprints (${activeSprints.length}):`;
142
+ if (activeSprints.length === 0) {
143
+ return head;
144
+ }
145
+ const blocks = activeSprints.map((block) => {
146
+ const sp = block.sprint;
147
+ const title = `Sprint '${sp.slug}' "${sp.name}" (id: ${shortId(sp.id)}):`;
148
+ const taskLines =
149
+ block.tasks.length === 0
150
+ ? " (no tasks)"
151
+ : block.tasks.map((t) => formatTaskLine(t, " ")).join("\n");
152
+ const summary = ` Summary: ${formatTaskCounts(block.taskCounts)}`;
153
+ return `${title}\n${taskLines}\n${summary}`;
154
+ });
155
+ return `${head}\n\n${blocks.join("\n\n")}`;
156
+ }
157
+
158
+ /**
159
+ * Single-sprint detail for `get_sprint_detail`.
160
+ */
161
+ /** Per-sprint roll-up for `get_plan_progress`. */
162
+ export type PlanProgressSprintRow = {
163
+ slug: string;
164
+ name: string;
165
+ status: Sprint["status"];
166
+ done: number;
167
+ total: number;
168
+ };
169
+
170
+ /**
171
+ * Overall completion summary for `get_plan_progress`.
172
+ */
173
+ export function formatPlanProgress(
174
+ planSlug: string,
175
+ done: number,
176
+ totalTasks: number,
177
+ sprints: PlanProgressSprintRow[]
178
+ ): string {
179
+ const pct =
180
+ totalTasks === 0 ? 0 : Math.round((done / totalTasks) * 100);
181
+ const head = `Plan '${planSlug}' progress: ${pct}% complete (${done}/${totalTasks} tasks done)`;
182
+ if (sprints.length === 0) {
183
+ return head;
184
+ }
185
+ const lines = sprints.map((s) => {
186
+ const spct = s.total === 0 ? 0 : Math.round((s.done / s.total) * 100);
187
+ return `- ${s.name} (${s.slug}) [${s.status}]: ${s.done}/${s.total} done (${spct}%)`;
188
+ });
189
+ return `${head}\n${lines.join("\n")}`;
190
+ }
191
+
192
+ export function formatSprintDetail(sprint: Sprint, tasks: Task[]): string {
193
+ const dates =
194
+ sprint.startDate || sprint.endDate
195
+ ? `\nDates: start=${sprint.startDate ?? "—"}, end=${sprint.endDate ?? "—"}`
196
+ : "";
197
+ const md =
198
+ sprint.markdownContent != null && sprint.markdownContent.trim().length > 0
199
+ ? `\n\n--- Markdown ---\n${sprint.markdownContent}`
200
+ : "";
201
+ const header = `Sprint '${sprint.name}' (${sprint.slug}) [${sprint.status}]\nGoal: ${sprint.goal}${dates}${md}`;
202
+ const body = formatTaskList(tasks, "Tasks");
203
+ return `${header}\n\n${body}`;
204
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Maps domain errors to MCP tool error responses.
5
+ */
6
+ import { BackendError } from "../errors/backend-errors.js";
7
+
8
+ export type McpToolErrorResult = {
9
+ content: [{ type: "text"; text: string }];
10
+ isError: true;
11
+ };
12
+
13
+ /**
14
+ * Converts known errors into MCP tool `isError` payloads; rethrows unknown non-Errors.
15
+ */
16
+ export function toMcpToolError(error: unknown): McpToolErrorResult {
17
+ if (error instanceof BackendError) {
18
+ return { content: [{ type: "text", text: error.message }], isError: true };
19
+ }
20
+ if (error instanceof Error) {
21
+ return { content: [{ type: "text", text: error.message }], isError: true };
22
+ }
23
+ throw error;
24
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: MCP plugin — execution context and sprint detail (read-heavy agent views).
5
+ */
6
+ import { z } from "zod";
7
+ import {
8
+ countByTaskStatus,
9
+ getSprintForPlan,
10
+ resolvePlanOrActive
11
+ } from "../mcp-query-helpers.js";
12
+ import {
13
+ formatExecutionContext,
14
+ formatSprintDetail
15
+ } from "../mcp-text-formatters.js";
16
+ import { toMcpToolError } from "../mcp-tool-error.js";
17
+ import type { SprintdockMcpToolPlugin } from "./types.js";
18
+
19
+ export const contextToolsPlugin: SprintdockMcpToolPlugin = {
20
+ id: "sprintdock/context-tools",
21
+ register(server, ctx) {
22
+ const { deps, kit } = ctx;
23
+ const { jsonStructured, optionalPlanSlug } = kit;
24
+ const { planService, sprintService, taskService } = deps.services;
25
+
26
+ server.registerTool(
27
+ "get_sprint_detail",
28
+ {
29
+ description:
30
+ "Loads one sprint by UUID or sprint slug within the resolved plan, including goal, dates, status, and a full task list with metadata. Prefer this over get_sprint when you need tasks.",
31
+ inputSchema: z
32
+ .object({
33
+ sprintIdOrSlug: z.string().min(1),
34
+ planSlug: optionalPlanSlug
35
+ })
36
+ .strict()
37
+ },
38
+ async (input) => {
39
+ try {
40
+ const plan = await resolvePlanOrActive(planService, input.planSlug);
41
+ const sprint = await getSprintForPlan(
42
+ sprintService,
43
+ plan,
44
+ input.sprintIdOrSlug
45
+ );
46
+ const tasks = await taskService.listTasks(sprint.id);
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: formatSprintDetail(sprint, tasks)
52
+ }
53
+ ],
54
+ structuredContent: jsonStructured({
55
+ sprintSlug: sprint.slug,
56
+ sprint,
57
+ tasks
58
+ })
59
+ };
60
+ } catch (e) {
61
+ return toMcpToolError(e);
62
+ }
63
+ }
64
+ );
65
+
66
+ server.registerTool(
67
+ "get_execution_context",
68
+ {
69
+ description:
70
+ "Returns the working plan plus every active sprint, each with its full task list and per-status counts. This is the main tool to see what to work on next.",
71
+ inputSchema: z.object({ planSlug: optionalPlanSlug }).strict()
72
+ },
73
+ async (input) => {
74
+ try {
75
+ const plan = await resolvePlanOrActive(planService, input.planSlug);
76
+ const all = await sprintService.listSprints(plan.id);
77
+ const activeSprints = [];
78
+ for (const sp of all) {
79
+ if (sp.status !== "active") continue;
80
+ const tasks = await taskService.listTasks(sp.id);
81
+ activeSprints.push({
82
+ sprintSlug: sp.slug,
83
+ sprint: sp,
84
+ tasks,
85
+ taskCounts: countByTaskStatus(tasks)
86
+ });
87
+ }
88
+ return {
89
+ content: [
90
+ {
91
+ type: "text",
92
+ text: formatExecutionContext(plan.slug, activeSprints)
93
+ }
94
+ ],
95
+ structuredContent: jsonStructured({
96
+ planSlug: plan.slug,
97
+ planTitle: plan.title,
98
+ activeSprints
99
+ })
100
+ };
101
+ } catch (e) {
102
+ return toMcpToolError(e);
103
+ }
104
+ }
105
+ );
106
+ }
107
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Default ordered plugin list for full Sprintdock MCP tool surface.
5
+ */
6
+ import { contextToolsPlugin } from "./context-tools.plugin.js";
7
+ import { planToolsPlugin } from "./plan-tools.plugin.js";
8
+ import { sprintToolsPlugin } from "./sprint-tools.plugin.js";
9
+ import { taskToolsPlugin } from "./task-tools.plugin.js";
10
+ import { taskWorkflowPlugin } from "./task-workflow.plugin.js";
11
+ import type { SprintdockMcpToolPlugin } from "./types.js";
12
+
13
+ /**
14
+ * Built-in plugins in registration order. Duplicate tool names across plugins will
15
+ * overwrite earlier registrations — keep each tool name unique per plugin set.
16
+ */
17
+ export const defaultSprintdockMcpPlugins: readonly SprintdockMcpToolPlugin[] = [
18
+ planToolsPlugin,
19
+ contextToolsPlugin,
20
+ sprintToolsPlugin,
21
+ taskToolsPlugin,
22
+ taskWorkflowPlugin
23
+ ] as const;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Barrel for Sprintdock MCP tool plugins and shared registration helpers.
5
+ */
6
+ export { contextToolsPlugin } from "./context-tools.plugin.js";
7
+ export { defaultSprintdockMcpPlugins } from "./default-plugins.js";
8
+ export {
9
+ createSprintdockMcpToolKit,
10
+ type SprintdockMcpToolKit
11
+ } from "./mcp-tool-kit.js";
12
+ export { planToolsPlugin } from "./plan-tools.plugin.js";
13
+ export { sprintToolsPlugin } from "./sprint-tools.plugin.js";
14
+ export { taskToolsPlugin } from "./task-tools.plugin.js";
15
+ export { taskWorkflowPlugin } from "./task-workflow.plugin.js";
16
+ export type {
17
+ RegisterSprintdockMcpToolsOptions,
18
+ SprintdockMcpPluginContext,
19
+ SprintdockMcpToolDependencies,
20
+ SprintdockMcpToolPlugin
21
+ } from "./types.js";