backlog-mcp 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Goga Koreli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # backlog-mcp
2
+
3
+ Minimal task backlog as an MCP server. Records state, doesn't enforce workflow.
4
+
5
+ ## Task Schema
6
+
7
+ ```typescript
8
+ {
9
+ id: string; // TASK-0001
10
+ title: string;
11
+ description?: string;
12
+ status: 'open' | 'in_progress' | 'blocked' | 'done' | 'cancelled';
13
+ created_at: string; // ISO8601
14
+ updated_at: string; // ISO8601
15
+ blocked_reason?: string;
16
+ evidence?: string[];
17
+ }
18
+ ```
19
+
20
+ ## MCP Tools
21
+
22
+ | Tool | Description |
23
+ |------|-------------|
24
+ | `backlog_list` | List tasks. Filter by status. Use `summary=true` for counts. |
25
+ | `backlog_get` | Get task by ID |
26
+ | `backlog_create` | Create task |
27
+ | `backlog_update` | Update any field (title, description, status, blocked_reason, evidence) |
28
+
29
+ ## Installation
30
+
31
+ Add to your MCP config (`.mcp.json` or Claude Desktop config):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "backlog": {
37
+ "command": "npx",
38
+ "args": ["-y", "backlog-mcp"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ Or build from source:
45
+
46
+ ```bash
47
+ git clone https://github.com/gkoreli/backlog-mcp.git
48
+ cd backlog-mcp
49
+ npm install && npm run build
50
+ npm start
51
+ ```
52
+
53
+ Claude Desktop config:
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "backlog": {
59
+ "command": "node",
60
+ "args": ["/path/to/backlog-mcp/dist/server.js"]
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Storage
67
+
68
+ Single file: `data/backlog.json` (atomic writes via temp + rename)
69
+
70
+ ## License
71
+
72
+ MIT
@@ -0,0 +1,2 @@
1
+ export { isValidTaskId, parseTaskId, formatTaskId, nextTaskId, STATUSES, type Status, type Task, type CreateTaskInput, createTask, } from './schema.js';
2
+ export { type Backlog, type StorageOptions, loadBacklog, saveBacklog, getTask, listTasks, addTask, saveTask, deleteTask, taskExists, getTaskCounts, } from './storage.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Schema
2
+ export { isValidTaskId, parseTaskId, formatTaskId, nextTaskId, STATUSES, createTask, } from './schema.js';
3
+ // Storage
4
+ export { loadBacklog, saveBacklog, getTask, listTasks, addTask, saveTask, deleteTask, taskExists, getTaskCounts, } from './storage.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,SAAS;AACT,OAAO,EACL,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,EACV,QAAQ,EAIR,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,UAAU;AACV,OAAO,EAGL,WAAW,EACX,WAAW,EACX,OAAO,EACP,SAAS,EACT,OAAO,EACP,QAAQ,EACR,UAAU,EACV,UAAU,EACV,aAAa,GACd,MAAM,cAAc,CAAC"}
@@ -0,0 +1,26 @@
1
+ export declare function isValidTaskId(id: unknown): id is string;
2
+ export declare function parseTaskId(id: string): number | null;
3
+ export declare function formatTaskId(num: number): string;
4
+ export declare function nextTaskId(existingTasks: ReadonlyArray<{
5
+ id: string;
6
+ }>): string;
7
+ export declare const STATUSES: readonly ["open", "in_progress", "blocked", "done", "cancelled"];
8
+ export type Status = (typeof STATUSES)[number];
9
+ export interface Task {
10
+ id: string;
11
+ title: string;
12
+ description?: string;
13
+ status: Status;
14
+ created_at: string;
15
+ updated_at: string;
16
+ blocked_reason?: string;
17
+ evidence?: string[];
18
+ }
19
+ export interface CreateTaskInput {
20
+ id?: string;
21
+ title: string;
22
+ description?: string;
23
+ }
24
+ export declare function createTask(input: CreateTaskInput, existingTasks?: ReadonlyArray<{
25
+ id: string;
26
+ }>): Task;
package/dist/schema.js ADDED
@@ -0,0 +1,42 @@
1
+ // ============================================================================
2
+ // Task ID
3
+ // ============================================================================
4
+ const TASK_ID_PATTERN = /^TASK-(\d{4,})$/;
5
+ export function isValidTaskId(id) {
6
+ return typeof id === 'string' && TASK_ID_PATTERN.test(id);
7
+ }
8
+ export function parseTaskId(id) {
9
+ const match = TASK_ID_PATTERN.exec(id);
10
+ if (!match?.[1])
11
+ return null;
12
+ return parseInt(match[1], 10);
13
+ }
14
+ export function formatTaskId(num) {
15
+ return `TASK-${num.toString().padStart(4, '0')}`;
16
+ }
17
+ export function nextTaskId(existingTasks) {
18
+ let maxNum = 0;
19
+ for (const task of existingTasks) {
20
+ const num = parseTaskId(task.id);
21
+ if (num !== null && num > maxNum) {
22
+ maxNum = num;
23
+ }
24
+ }
25
+ return formatTaskId(maxNum + 1);
26
+ }
27
+ // ============================================================================
28
+ // Status
29
+ // ============================================================================
30
+ export const STATUSES = ['open', 'in_progress', 'blocked', 'done', 'cancelled'];
31
+ export function createTask(input, existingTasks = []) {
32
+ const now = new Date().toISOString();
33
+ return {
34
+ id: input.id ?? nextTaskId(existingTasks),
35
+ title: input.title,
36
+ description: input.description,
37
+ status: 'open',
38
+ created_at: now,
39
+ updated_at: now,
40
+ };
41
+ }
42
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,UAAU;AACV,+EAA+E;AAE/E,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAE1C,MAAM,UAAU,aAAa,CAAC,EAAW;IACvC,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7B,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,OAAO,QAAQ,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,aAA4C;IACrE,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjC,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,GAAG,MAAM,EAAE,CAAC;YACjC,MAAM,GAAG,GAAG,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,+EAA+E;AAC/E,SAAS;AACT,+EAA+E;AAE/E,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,CAAU,CAAC;AA4BzF,MAAM,UAAU,UAAU,CACxB,KAAsB,EACtB,gBAA+C,EAAE;IAEjD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,OAAO;QACL,EAAE,EAAE,KAAK,CAAC,EAAE,IAAI,UAAU,CAAC,aAAa,CAAC;QACzC,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,MAAM,EAAE,MAAM;QACd,UAAU,EAAE,GAAG;QACf,UAAU,EAAE,GAAG;KAChB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { createTask, STATUSES } from './schema.js';
6
+ import { loadBacklog, getTask, listTasks, addTask, saveTask, getTaskCounts } from './storage.js';
7
+ // ============================================================================
8
+ // Server
9
+ // ============================================================================
10
+ const server = new McpServer({
11
+ name: 'backlog-mcp',
12
+ version: '0.1.0',
13
+ });
14
+ const storageOptions = {
15
+ dataDir: process.env.BACKLOG_DATA_DIR ?? 'data',
16
+ };
17
+ // ============================================================================
18
+ // Tools
19
+ // ============================================================================
20
+ server.registerTool('backlog_list', {
21
+ description: 'List tasks, optionally filtered by status. Use summary=true for counts.',
22
+ inputSchema: {
23
+ status: z.array(z.enum(STATUSES)).optional().describe('Filter by status'),
24
+ summary: z.boolean().optional().describe('Return counts instead of list'),
25
+ },
26
+ }, async ({ status, summary }) => {
27
+ const tasks = listTasks(status ? { status } : undefined, storageOptions);
28
+ if (summary) {
29
+ const counts = getTaskCounts(storageOptions);
30
+ return { content: [{ type: 'text', text: JSON.stringify(counts, null, 2) }] };
31
+ }
32
+ const list = tasks.map((t) => ({ id: t.id, title: t.title, status: t.status }));
33
+ return { content: [{ type: 'text', text: JSON.stringify(list, null, 2) }] };
34
+ });
35
+ server.registerTool('backlog_get', {
36
+ description: 'Get a task by ID',
37
+ inputSchema: { id: z.string().describe('Task ID') },
38
+ }, async ({ id }) => {
39
+ const task = getTask(id, storageOptions);
40
+ if (!task) {
41
+ return { content: [{ type: 'text', text: `Not found: ${id}` }], isError: true };
42
+ }
43
+ return { content: [{ type: 'text', text: JSON.stringify(task, null, 2) }] };
44
+ });
45
+ server.registerTool('backlog_create', {
46
+ description: 'Create a new task',
47
+ inputSchema: {
48
+ title: z.string().describe('Task title'),
49
+ description: z.string().optional().describe('Task description'),
50
+ },
51
+ }, async ({ title, description }) => {
52
+ const backlog = loadBacklog(storageOptions);
53
+ const task = createTask({ title, description }, backlog.tasks);
54
+ addTask(task, storageOptions);
55
+ return { content: [{ type: 'text', text: `Created ${task.id}` }] };
56
+ });
57
+ server.registerTool('backlog_update', {
58
+ description: 'Update a task (any field)',
59
+ inputSchema: {
60
+ id: z.string().describe('Task ID'),
61
+ title: z.string().optional(),
62
+ description: z.string().optional(),
63
+ status: z.enum(STATUSES).optional(),
64
+ blocked_reason: z.string().optional(),
65
+ evidence: z.array(z.string()).optional(),
66
+ },
67
+ }, async ({ id, ...updates }) => {
68
+ const task = getTask(id, storageOptions);
69
+ if (!task) {
70
+ return { content: [{ type: 'text', text: `Not found: ${id}` }], isError: true };
71
+ }
72
+ const updated = {
73
+ ...task,
74
+ ...Object.fromEntries(Object.entries(updates).filter(([_, v]) => v !== undefined)),
75
+ updated_at: new Date().toISOString(),
76
+ };
77
+ saveTask(updated, storageOptions);
78
+ return { content: [{ type: 'text', text: `Updated ${id}` }] };
79
+ });
80
+ // ============================================================================
81
+ // Main
82
+ // ============================================================================
83
+ async function main() {
84
+ const transport = new StdioServerTransport();
85
+ await server.connect(transport);
86
+ }
87
+ main().catch((error) => {
88
+ console.error('Fatal error:', error);
89
+ process.exit(1);
90
+ });
91
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import type { Task } from './schema.js';
2
+ export interface Backlog {
3
+ version: '1';
4
+ tasks: Task[];
5
+ }
6
+ export interface StorageOptions {
7
+ dataDir?: string;
8
+ }
9
+ /**
10
+ * Load backlog from disk. Returns empty backlog if file doesn't exist.
11
+ */
12
+ export declare function loadBacklog(options?: StorageOptions): Backlog;
13
+ /**
14
+ * Save backlog to disk atomically (write to temp, then rename).
15
+ */
16
+ export declare function saveBacklog(backlog: Backlog, options?: StorageOptions): void;
17
+ /**
18
+ * Load archive from disk. Returns empty backlog if file doesn't exist.
19
+ */
20
+ export declare function loadArchive(options?: StorageOptions): Backlog;
21
+ /**
22
+ * Get a task by ID. Returns undefined if not found.
23
+ */
24
+ export declare function getTask(id: string, options?: StorageOptions): Task | undefined;
25
+ /**
26
+ * List all tasks. Optionally filter by status.
27
+ */
28
+ export declare function listTasks(filter?: {
29
+ status?: Task['status'][];
30
+ }, options?: StorageOptions): Task[];
31
+ /**
32
+ * Add a new task. Throws if task with same ID already exists.
33
+ */
34
+ export declare function addTask(task: Task, options?: StorageOptions): void;
35
+ /**
36
+ * Update an existing task. Throws if task doesn't exist.
37
+ * Automatically archives tasks with terminal status (done, cancelled).
38
+ */
39
+ export declare function saveTask(task: Task, options?: StorageOptions): void;
40
+ /**
41
+ * Delete a task by ID. Throws if task doesn't exist.
42
+ */
43
+ export declare function deleteTask(id: string, options?: StorageOptions): void;
44
+ /**
45
+ * Check if a task exists.
46
+ */
47
+ export declare function taskExists(id: string, options?: StorageOptions): boolean;
48
+ /**
49
+ * Get count of tasks by status.
50
+ */
51
+ export declare function getTaskCounts(options?: StorageOptions): Record<Task['status'], number>;
@@ -0,0 +1,194 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ // ============================================================================
5
+ // Storage
6
+ // ============================================================================
7
+ const DEFAULT_DATA_DIR = 'data';
8
+ const BACKLOG_FILE = 'backlog.json';
9
+ const ARCHIVE_FILE = 'archive.json';
10
+ const TERMINAL_STATUSES = ['done', 'cancelled'];
11
+ function ensureDir(dir) {
12
+ if (!existsSync(dir)) {
13
+ mkdirSync(dir, { recursive: true });
14
+ }
15
+ }
16
+ function getBacklogPath(dataDir) {
17
+ return join(dataDir, BACKLOG_FILE);
18
+ }
19
+ function getArchivePath(dataDir) {
20
+ return join(dataDir, ARCHIVE_FILE);
21
+ }
22
+ function emptyBacklog() {
23
+ return {
24
+ version: '1',
25
+ tasks: [],
26
+ };
27
+ }
28
+ /**
29
+ * Load backlog from disk. Returns empty backlog if file doesn't exist.
30
+ */
31
+ export function loadBacklog(options = {}) {
32
+ const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
33
+ const path = getBacklogPath(dataDir);
34
+ if (!existsSync(path)) {
35
+ return emptyBacklog();
36
+ }
37
+ const content = readFileSync(path, 'utf-8');
38
+ const data = JSON.parse(content);
39
+ // Basic version check
40
+ if (data.version !== '1') {
41
+ throw new Error(`Unsupported backlog version: ${data.version}`);
42
+ }
43
+ return data;
44
+ }
45
+ /**
46
+ * Save backlog to disk atomically (write to temp, then rename).
47
+ */
48
+ export function saveBacklog(backlog, options = {}) {
49
+ const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
50
+ ensureDir(dataDir);
51
+ const path = getBacklogPath(dataDir);
52
+ const tempPath = join(dataDir, `.backlog.${randomUUID()}.tmp`);
53
+ const content = JSON.stringify(backlog, null, 2);
54
+ // Write to temp file first
55
+ writeFileSync(tempPath, content, 'utf-8');
56
+ // Atomic rename
57
+ renameSync(tempPath, path);
58
+ }
59
+ /**
60
+ * Load archive from disk. Returns empty backlog if file doesn't exist.
61
+ */
62
+ export function loadArchive(options = {}) {
63
+ const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
64
+ const path = getArchivePath(dataDir);
65
+ if (!existsSync(path)) {
66
+ return emptyBacklog();
67
+ }
68
+ const content = readFileSync(path, 'utf-8');
69
+ const data = JSON.parse(content);
70
+ if (data.version !== '1') {
71
+ throw new Error(`Unsupported archive version: ${data.version}`);
72
+ }
73
+ return data;
74
+ }
75
+ /**
76
+ * Save archive to disk atomically.
77
+ */
78
+ function saveArchive(archive, options = {}) {
79
+ const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
80
+ ensureDir(dataDir);
81
+ const path = getArchivePath(dataDir);
82
+ const tempPath = join(dataDir, `.archive.${randomUUID()}.tmp`);
83
+ const content = JSON.stringify(archive, null, 2);
84
+ writeFileSync(tempPath, content, 'utf-8');
85
+ renameSync(tempPath, path);
86
+ }
87
+ /**
88
+ * Move a task from backlog to archive.
89
+ */
90
+ function archiveTask(task, options = {}) {
91
+ const backlog = loadBacklog(options);
92
+ const archive = loadArchive(options);
93
+ // Remove from backlog
94
+ const index = backlog.tasks.findIndex((t) => t.id === task.id);
95
+ if (index !== -1) {
96
+ backlog.tasks.splice(index, 1);
97
+ }
98
+ // Add to archive (replace if exists)
99
+ const archiveIndex = archive.tasks.findIndex((t) => t.id === task.id);
100
+ if (archiveIndex !== -1) {
101
+ archive.tasks[archiveIndex] = task;
102
+ }
103
+ else {
104
+ archive.tasks.push(task);
105
+ }
106
+ saveBacklog(backlog, options);
107
+ saveArchive(archive, options);
108
+ }
109
+ // ============================================================================
110
+ // Task Operations
111
+ // ============================================================================
112
+ /**
113
+ * Get a task by ID. Returns undefined if not found.
114
+ */
115
+ export function getTask(id, options = {}) {
116
+ const backlog = loadBacklog(options);
117
+ return backlog.tasks.find((t) => t.id === id);
118
+ }
119
+ /**
120
+ * List all tasks. Optionally filter by status.
121
+ */
122
+ export function listTasks(filter, options = {}) {
123
+ const backlog = loadBacklog(options);
124
+ if (filter?.status && filter.status.length > 0) {
125
+ return backlog.tasks.filter((t) => filter.status.includes(t.status));
126
+ }
127
+ return backlog.tasks;
128
+ }
129
+ /**
130
+ * Add a new task. Throws if task with same ID already exists.
131
+ */
132
+ export function addTask(task, options = {}) {
133
+ const backlog = loadBacklog(options);
134
+ if (backlog.tasks.some((t) => t.id === task.id)) {
135
+ throw new Error(`Task with ID '${task.id}' already exists`);
136
+ }
137
+ backlog.tasks.push(task);
138
+ saveBacklog(backlog, options);
139
+ }
140
+ /**
141
+ * Update an existing task. Throws if task doesn't exist.
142
+ * Automatically archives tasks with terminal status (done, cancelled).
143
+ */
144
+ export function saveTask(task, options = {}) {
145
+ const backlog = loadBacklog(options);
146
+ const index = backlog.tasks.findIndex((t) => t.id === task.id);
147
+ if (index === -1) {
148
+ throw new Error(`Task with ID '${task.id}' not found`);
149
+ }
150
+ // Archive if terminal status
151
+ if (TERMINAL_STATUSES.includes(task.status)) {
152
+ archiveTask(task, options);
153
+ return;
154
+ }
155
+ backlog.tasks[index] = task;
156
+ saveBacklog(backlog, options);
157
+ }
158
+ /**
159
+ * Delete a task by ID. Throws if task doesn't exist.
160
+ */
161
+ export function deleteTask(id, options = {}) {
162
+ const backlog = loadBacklog(options);
163
+ const index = backlog.tasks.findIndex((t) => t.id === id);
164
+ if (index === -1) {
165
+ throw new Error(`Task with ID '${id}' not found`);
166
+ }
167
+ backlog.tasks.splice(index, 1);
168
+ saveBacklog(backlog, options);
169
+ }
170
+ /**
171
+ * Check if a task exists.
172
+ */
173
+ export function taskExists(id, options = {}) {
174
+ const backlog = loadBacklog(options);
175
+ return backlog.tasks.some((t) => t.id === id);
176
+ }
177
+ /**
178
+ * Get count of tasks by status.
179
+ */
180
+ export function getTaskCounts(options = {}) {
181
+ const backlog = loadBacklog(options);
182
+ const counts = {
183
+ open: 0,
184
+ in_progress: 0,
185
+ blocked: 0,
186
+ done: 0,
187
+ cancelled: 0,
188
+ };
189
+ for (const task of backlog.tasks) {
190
+ counts[task.status]++;
191
+ }
192
+ return counts;
193
+ }
194
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { Task, Status, Blocked, Dod, Evidence, ValidationError } from './schema.js';
2
+ export declare const TERMINAL_STATUSES: readonly Status[];
3
+ export interface TransitionToBlocked {
4
+ to: 'blocked';
5
+ blocked: Blocked;
6
+ }
7
+ export interface TransitionToVerifying {
8
+ to: 'verifying';
9
+ dod: Dod;
10
+ evidence: Evidence;
11
+ }
12
+ export interface TransitionToDone {
13
+ to: 'done';
14
+ }
15
+ export interface TransitionToInProgress {
16
+ to: 'in_progress';
17
+ }
18
+ export interface TransitionToCancelled {
19
+ to: 'cancelled';
20
+ }
21
+ export type TransitionInput = TransitionToBlocked | TransitionToVerifying | TransitionToDone | TransitionToInProgress | TransitionToCancelled;
22
+ export type TransitionResult = {
23
+ ok: true;
24
+ task: Task;
25
+ } | {
26
+ ok: false;
27
+ errors: ValidationError[];
28
+ };
29
+ export declare function canTransition(from: Status, to: Status): boolean;
30
+ export declare function getAllowedTransitions(from: Status): readonly Status[];
31
+ export declare function isTerminal(status: Status): boolean;
32
+ export declare function transition(task: Task, input: TransitionInput): TransitionResult;
@@ -0,0 +1,174 @@
1
+ // ============================================================================
2
+ // Constants
3
+ // ============================================================================
4
+ export const TERMINAL_STATUSES = ['done', 'cancelled'];
5
+ /**
6
+ * State machine transitions:
7
+ *
8
+ * open ──────► in_progress ──────► verifying ──────► done
9
+ * │ │ │
10
+ * │ │ ├─► in_progress (rejected)
11
+ * │ │ │
12
+ * │ │ └─► cancelled
13
+ * │ │
14
+ * │ ├────────────────► blocked
15
+ * │ │ │
16
+ * │ │ ├─► in_progress
17
+ * │ │ │
18
+ * │ │ └─► cancelled
19
+ * │ │
20
+ * │ └────────────────► cancelled
21
+ * │
22
+ * └────────────────────────────────► cancelled
23
+ */
24
+ const ALLOWED_TRANSITIONS = {
25
+ open: ['in_progress', 'cancelled'],
26
+ in_progress: ['blocked', 'verifying', 'cancelled'],
27
+ blocked: ['in_progress', 'cancelled'],
28
+ verifying: ['done', 'in_progress', 'cancelled'],
29
+ done: [],
30
+ cancelled: [],
31
+ };
32
+ // ============================================================================
33
+ // Helpers
34
+ // ============================================================================
35
+ function isNonEmptyString(value) {
36
+ return typeof value === 'string' && value.trim().length > 0;
37
+ }
38
+ function isNonEmptyStringArray(value) {
39
+ return Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString);
40
+ }
41
+ // ============================================================================
42
+ // Query Functions
43
+ // ============================================================================
44
+ export function canTransition(from, to) {
45
+ return ALLOWED_TRANSITIONS[from].includes(to);
46
+ }
47
+ export function getAllowedTransitions(from) {
48
+ return ALLOWED_TRANSITIONS[from];
49
+ }
50
+ export function isTerminal(status) {
51
+ return TERMINAL_STATUSES.includes(status);
52
+ }
53
+ // ============================================================================
54
+ // Transition Validators
55
+ // ============================================================================
56
+ function validateBlockedInput(input, errors) {
57
+ if (!input.blocked || typeof input.blocked !== 'object') {
58
+ errors.push({ field: 'blocked', message: 'required for transition to blocked' });
59
+ return;
60
+ }
61
+ if (!isNonEmptyString(input.blocked.reason)) {
62
+ errors.push({ field: 'blocked.reason', message: 'must be a non-empty string' });
63
+ }
64
+ if (input.blocked.dependency !== undefined && typeof input.blocked.dependency !== 'string') {
65
+ errors.push({ field: 'blocked.dependency', message: 'must be a string if present' });
66
+ }
67
+ }
68
+ function validateVerifyingInput(input, errors) {
69
+ // Validate dod
70
+ if (!input.dod || typeof input.dod !== 'object') {
71
+ errors.push({ field: 'dod', message: 'required for transition to verifying' });
72
+ }
73
+ else if (!isNonEmptyStringArray(input.dod.checklist)) {
74
+ errors.push({ field: 'dod.checklist', message: 'must be a non-empty array of non-empty strings' });
75
+ }
76
+ // Validate evidence
77
+ if (!input.evidence || typeof input.evidence !== 'object') {
78
+ errors.push({ field: 'evidence', message: 'required for transition to verifying' });
79
+ return;
80
+ }
81
+ if (!isNonEmptyStringArray(input.evidence.artifacts)) {
82
+ errors.push({ field: 'evidence.artifacts', message: 'must have at least 1 non-empty artifact' });
83
+ }
84
+ if (input.evidence.commands !== undefined) {
85
+ if (!Array.isArray(input.evidence.commands)) {
86
+ errors.push({ field: 'evidence.commands', message: 'must be an array if present' });
87
+ }
88
+ else if (!input.evidence.commands.every((c) => typeof c === 'string')) {
89
+ errors.push({ field: 'evidence.commands', message: 'all items must be strings' });
90
+ }
91
+ }
92
+ if (input.evidence.notes !== undefined && typeof input.evidence.notes !== 'string') {
93
+ errors.push({ field: 'evidence.notes', message: 'must be a string if present' });
94
+ }
95
+ }
96
+ function validateDoneTransition(task, errors) {
97
+ // Structural verification: task must already have dod and evidence from verifying state
98
+ if (!task.dod || !Array.isArray(task.dod.checklist) || task.dod.checklist.length === 0) {
99
+ errors.push({ field: 'dod.checklist', message: 'must be non-empty to complete verification' });
100
+ }
101
+ if (!task.evidence || !Array.isArray(task.evidence.artifacts) || task.evidence.artifacts.length === 0) {
102
+ errors.push({ field: 'evidence.artifacts', message: 'must have at least 1 artifact to complete verification' });
103
+ }
104
+ }
105
+ // ============================================================================
106
+ // Transition Function
107
+ // ============================================================================
108
+ export function transition(task, input) {
109
+ const errors = [];
110
+ const from = task.status;
111
+ const to = input.to;
112
+ // Check if transition is allowed by state machine
113
+ if (!canTransition(from, to)) {
114
+ const allowed = getAllowedTransitions(from);
115
+ const allowedStr = allowed.length > 0 ? allowed.join(', ') : 'none (terminal state)';
116
+ errors.push({
117
+ field: 'status',
118
+ message: `cannot transition from '${from}' to '${to}'. Allowed: ${allowedStr}`,
119
+ });
120
+ return { ok: false, errors };
121
+ }
122
+ // Validate transition-specific requirements
123
+ switch (input.to) {
124
+ case 'blocked':
125
+ validateBlockedInput(input, errors);
126
+ break;
127
+ case 'verifying':
128
+ validateVerifyingInput(input, errors);
129
+ break;
130
+ case 'done':
131
+ validateDoneTransition(task, errors);
132
+ break;
133
+ }
134
+ if (errors.length > 0) {
135
+ return { ok: false, errors };
136
+ }
137
+ // Build the updated task
138
+ const now = new Date().toISOString();
139
+ const updatedTask = {
140
+ ...task,
141
+ status: to,
142
+ updated_at: now,
143
+ };
144
+ // Apply transition-specific changes
145
+ switch (input.to) {
146
+ case 'blocked':
147
+ updatedTask.blocked = input.blocked;
148
+ break;
149
+ case 'verifying':
150
+ updatedTask.dod = input.dod;
151
+ updatedTask.evidence = input.evidence;
152
+ updatedTask.blocked = null;
153
+ break;
154
+ case 'done':
155
+ // No changes needed - dod and evidence already set from verifying
156
+ updatedTask.blocked = null;
157
+ break;
158
+ case 'in_progress':
159
+ if (from === 'blocked') {
160
+ // Clear blocked field when unblocking
161
+ updatedTask.blocked = null;
162
+ }
163
+ else if (from === 'verifying') {
164
+ // Rejection: clear evidence, keep dod for retry
165
+ updatedTask.evidence = undefined;
166
+ }
167
+ break;
168
+ case 'cancelled':
169
+ updatedTask.blocked = null;
170
+ break;
171
+ }
172
+ return { ok: true, task: updatedTask };
173
+ }
174
+ //# sourceMappingURL=transitions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transitions.js","sourceRoot":"","sources":["../src/transitions.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "backlog-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Minimal, boring, correct core for persistent backlog state",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "backlog-mcp": "./dist/server.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "clean": "rm -rf dist",
25
+ "start": "node dist/server.js",
26
+ "test": "tsx --test src/**/*.test.ts",
27
+ "dev": "tsx watch src/server.ts"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "tsx": "^4.19.0",
36
+ "typescript": "^5.7.0"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.25.1"
40
+ }
41
+ }