@allpepper/task-orchestrator 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.
Files changed (42) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/src/db/client.ts +34 -0
  4. package/src/db/index.ts +1 -0
  5. package/src/db/migrate.ts +51 -0
  6. package/src/db/migrations/001_initial_schema.sql +160 -0
  7. package/src/domain/index.ts +1 -0
  8. package/src/domain/types.ts +225 -0
  9. package/src/index.ts +7 -0
  10. package/src/repos/base.ts +151 -0
  11. package/src/repos/dependencies.ts +356 -0
  12. package/src/repos/features.ts +507 -0
  13. package/src/repos/index.ts +4 -0
  14. package/src/repos/projects.ts +350 -0
  15. package/src/repos/sections.ts +505 -0
  16. package/src/repos/tags.example.ts +125 -0
  17. package/src/repos/tags.ts +175 -0
  18. package/src/repos/tasks.ts +581 -0
  19. package/src/repos/templates.ts +649 -0
  20. package/src/server.ts +121 -0
  21. package/src/services/index.ts +2 -0
  22. package/src/services/status-validator.ts +100 -0
  23. package/src/services/workflow.ts +104 -0
  24. package/src/tools/apply-template.ts +129 -0
  25. package/src/tools/get-blocked-tasks.ts +63 -0
  26. package/src/tools/get-next-status.ts +183 -0
  27. package/src/tools/get-next-task.ts +75 -0
  28. package/src/tools/get-tag-usage.ts +54 -0
  29. package/src/tools/index.ts +30 -0
  30. package/src/tools/list-tags.ts +56 -0
  31. package/src/tools/manage-container.ts +333 -0
  32. package/src/tools/manage-dependency.ts +198 -0
  33. package/src/tools/manage-sections.ts +388 -0
  34. package/src/tools/manage-template.ts +313 -0
  35. package/src/tools/query-container.ts +296 -0
  36. package/src/tools/query-dependencies.ts +68 -0
  37. package/src/tools/query-sections.ts +70 -0
  38. package/src/tools/query-templates.ts +137 -0
  39. package/src/tools/query-workflow-state.ts +198 -0
  40. package/src/tools/registry.ts +180 -0
  41. package/src/tools/rename-tag.ts +64 -0
  42. package/src/tools/setup-project.ts +189 -0
@@ -0,0 +1,333 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { createSuccessResponse, createErrorResponse, uuidSchema, optionalUuidSchema } from './registry';
4
+ import {
5
+ createProject,
6
+ getProject,
7
+ updateProject,
8
+ deleteProject,
9
+ } from '../repos/projects';
10
+ import {
11
+ createFeature,
12
+ getFeature,
13
+ updateFeature,
14
+ deleteFeature,
15
+ } from '../repos/features';
16
+ import {
17
+ createTask,
18
+ getTask,
19
+ updateTask,
20
+ deleteTask,
21
+ setTaskStatus,
22
+ } from '../repos/tasks';
23
+ import { ProjectStatus, FeatureStatus, TaskStatus, Priority } from '../domain/types';
24
+
25
+ /**
26
+ * Unified manage_container tool - write operations for containers
27
+ *
28
+ * Operations:
29
+ * - create: Create a new container
30
+ * - update: Update an existing container (requires version)
31
+ * - delete: Delete a container
32
+ * - setStatus: Update status only (requires version)
33
+ */
34
+ export function registerManageContainerTool(server: McpServer): void {
35
+ server.tool(
36
+ 'manage_container',
37
+ 'Unified write operations for containers (project, feature, task). Supports create, update, delete, and setStatus operations.',
38
+ {
39
+ operation: z.enum(['create', 'update', 'delete', 'setStatus']),
40
+ containerType: z.enum(['project', 'feature', 'task']),
41
+ id: optionalUuidSchema,
42
+ // Naming convention: projects/features use `name`, tasks use `title`.
43
+ // Both are the primary display label for their respective container types.
44
+ name: z.string().optional().describe('Display name for project and feature containers. Not used for tasks — use \'title\' instead.'),
45
+ title: z.string().optional().describe('Display title for task containers. Not used for projects/features — use \'name\' instead.'),
46
+ summary: z.string().optional(),
47
+ description: z.string().optional(),
48
+ status: z.string().optional(),
49
+ priority: z.string().optional(),
50
+ projectId: optionalUuidSchema,
51
+ featureId: optionalUuidSchema,
52
+ complexity: z.number().int().optional(),
53
+ tags: z.string().optional(),
54
+ version: z.number().int().optional(),
55
+ },
56
+ async (params) => {
57
+ try {
58
+ const { operation, containerType, id } = params;
59
+
60
+ // ===== CREATE OPERATION =====
61
+ if (operation === 'create') {
62
+ // Projects and features use 'name', tasks use 'title'
63
+ const nameOrTitle =
64
+ containerType === 'task'
65
+ ? params.title
66
+ : params.name;
67
+
68
+ if (!nameOrTitle) {
69
+ const fieldName = containerType === 'task' ? 'title' : 'name';
70
+ const response = createErrorResponse(`${fieldName} is required for create operation`);
71
+ return {
72
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
73
+ };
74
+ }
75
+
76
+ if (!params.summary) {
77
+ const response = createErrorResponse('summary is required for create operation');
78
+ return {
79
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
80
+ };
81
+ }
82
+
83
+ // Parse tags if provided
84
+ const tags = params.tags ? params.tags.split(',').map((t) => t.trim()) : undefined;
85
+
86
+ let result;
87
+ if (containerType === 'project') {
88
+ result = createProject({
89
+ name: nameOrTitle,
90
+ summary: params.summary,
91
+ description: params.description,
92
+ status: params.status as ProjectStatus | undefined,
93
+ tags,
94
+ });
95
+ } else if (containerType === 'feature') {
96
+ if (!params.priority) {
97
+ const response = createErrorResponse('priority is required for creating a feature');
98
+ return {
99
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
100
+ };
101
+ }
102
+
103
+ result = createFeature({
104
+ projectId: params.projectId,
105
+ name: nameOrTitle,
106
+ summary: params.summary,
107
+ description: params.description,
108
+ status: params.status as FeatureStatus | undefined,
109
+ priority: params.priority as Priority,
110
+ tags,
111
+ });
112
+ } else {
113
+ // task
114
+ if (!params.priority) {
115
+ const response = createErrorResponse('priority is required for creating a task');
116
+ return {
117
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
118
+ };
119
+ }
120
+
121
+ if (params.complexity === undefined) {
122
+ const response = createErrorResponse('complexity is required for creating a task');
123
+ return {
124
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
125
+ };
126
+ }
127
+
128
+ result = createTask({
129
+ projectId: params.projectId,
130
+ featureId: params.featureId,
131
+ title: nameOrTitle,
132
+ summary: params.summary,
133
+ description: params.description,
134
+ status: params.status as TaskStatus | undefined,
135
+ priority: params.priority as Priority,
136
+ complexity: params.complexity,
137
+ tags,
138
+ });
139
+ }
140
+
141
+ if (!result.success) {
142
+ const response = createErrorResponse(result.error || 'Failed to create container');
143
+ return {
144
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
145
+ };
146
+ }
147
+
148
+ const response = createSuccessResponse(
149
+ `${containerType} created successfully`,
150
+ { [containerType]: result.data }
151
+ );
152
+ return {
153
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
154
+ };
155
+ }
156
+
157
+ // ===== UPDATE OPERATION =====
158
+ if (operation === 'update') {
159
+ if (!id) {
160
+ const response = createErrorResponse('id is required for update operation');
161
+ return {
162
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
163
+ };
164
+ }
165
+
166
+ if (params.version === undefined) {
167
+ const response = createErrorResponse('version is required for update operation');
168
+ return {
169
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
170
+ };
171
+ }
172
+
173
+ // Parse tags if provided
174
+ const tags = params.tags ? params.tags.split(',').map((t) => t.trim()) : undefined;
175
+
176
+ let result;
177
+ if (containerType === 'project') {
178
+ result = updateProject(id, {
179
+ name: params.name,
180
+ summary: params.summary,
181
+ description: params.description,
182
+ status: params.status as ProjectStatus | undefined,
183
+ tags,
184
+ version: params.version,
185
+ });
186
+ } else if (containerType === 'feature') {
187
+ result = updateFeature(id, {
188
+ name: params.name,
189
+ summary: params.summary,
190
+ description: params.description,
191
+ status: params.status as FeatureStatus | undefined,
192
+ priority: params.priority as Priority | undefined,
193
+ projectId: params.projectId,
194
+ tags,
195
+ version: params.version,
196
+ });
197
+ } else {
198
+ // task
199
+ result = updateTask(id, {
200
+ title: params.title,
201
+ summary: params.summary,
202
+ description: params.description,
203
+ status: params.status as TaskStatus | undefined,
204
+ priority: params.priority as Priority | undefined,
205
+ complexity: params.complexity,
206
+ projectId: params.projectId,
207
+ featureId: params.featureId,
208
+ tags,
209
+ version: params.version,
210
+ });
211
+ }
212
+
213
+ if (!result.success) {
214
+ const response = createErrorResponse(result.error || 'Failed to update container');
215
+ return {
216
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
217
+ };
218
+ }
219
+
220
+ const response = createSuccessResponse(
221
+ `${containerType} updated successfully`,
222
+ { [containerType]: result.data }
223
+ );
224
+ return {
225
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
226
+ };
227
+ }
228
+
229
+ // ===== DELETE OPERATION =====
230
+ if (operation === 'delete') {
231
+ if (!id) {
232
+ const response = createErrorResponse('id is required for delete operation');
233
+ return {
234
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
235
+ };
236
+ }
237
+
238
+ let result;
239
+ if (containerType === 'project') {
240
+ result = deleteProject(id);
241
+ } else if (containerType === 'feature') {
242
+ result = deleteFeature(id);
243
+ } else {
244
+ result = deleteTask(id);
245
+ }
246
+
247
+ if (!result.success) {
248
+ const response = createErrorResponse(result.error || 'Failed to delete container');
249
+ return {
250
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
251
+ };
252
+ }
253
+
254
+ const response = createSuccessResponse(
255
+ `${containerType} deleted successfully`,
256
+ { deleted: true }
257
+ );
258
+ return {
259
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
260
+ };
261
+ }
262
+
263
+ // ===== SET STATUS OPERATION =====
264
+ if (operation === 'setStatus') {
265
+ if (!id) {
266
+ const response = createErrorResponse('id is required for setStatus operation');
267
+ return {
268
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
269
+ };
270
+ }
271
+
272
+ if (!params.status) {
273
+ const response = createErrorResponse('status is required for setStatus operation');
274
+ return {
275
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
276
+ };
277
+ }
278
+
279
+ if (params.version === undefined) {
280
+ const response = createErrorResponse('version is required for setStatus operation');
281
+ return {
282
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
283
+ };
284
+ }
285
+
286
+ let result;
287
+ if (containerType === 'task') {
288
+ // Tasks have a dedicated setTaskStatus function
289
+ result = setTaskStatus(id, params.status as TaskStatus, params.version);
290
+ } else {
291
+ // For projects and features, use update with only status
292
+ if (containerType === 'project') {
293
+ result = updateProject(id, {
294
+ status: params.status as ProjectStatus,
295
+ version: params.version,
296
+ });
297
+ } else {
298
+ result = updateFeature(id, {
299
+ status: params.status as FeatureStatus,
300
+ version: params.version,
301
+ });
302
+ }
303
+ }
304
+
305
+ if (!result.success) {
306
+ const response = createErrorResponse(result.error || 'Failed to set status');
307
+ return {
308
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
309
+ };
310
+ }
311
+
312
+ const response = createSuccessResponse(
313
+ `${containerType} status updated successfully`,
314
+ { [containerType]: result.data }
315
+ );
316
+ return {
317
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
318
+ };
319
+ }
320
+
321
+ const response = createErrorResponse('Invalid operation');
322
+ return {
323
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
324
+ };
325
+ } catch (error: any) {
326
+ const response = createErrorResponse(error.message || 'Internal error');
327
+ return {
328
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
329
+ };
330
+ }
331
+ }
332
+ );
333
+ }
@@ -0,0 +1,198 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { createSuccessResponse, createErrorResponse, uuidSchema, optionalUuidSchema } from './registry';
4
+ import { createDependency, deleteDependency } from '../repos/dependencies';
5
+ import { DependencyType } from '../domain/types';
6
+
7
+ /**
8
+ * Register the manage_dependency MCP tool.
9
+ *
10
+ * Manages dependencies between tasks with create and delete operations.
11
+ *
12
+ * @param server - The MCP server instance to register the tool with
13
+ */
14
+ export function registerManageDependencyTool(server: McpServer): void {
15
+ server.tool(
16
+ 'manage_dependency',
17
+ 'Manage dependencies between tasks (create, delete)',
18
+ {
19
+ operation: z
20
+ .enum(['create', 'delete'])
21
+ .describe('Operation to perform: create or delete'),
22
+ id: optionalUuidSchema
23
+ .describe('Dependency ID (required for delete operation)'),
24
+ fromTaskId: optionalUuidSchema
25
+ .describe('Source task ID (required for create operation)'),
26
+ toTaskId: optionalUuidSchema
27
+ .describe('Target task ID (required for create operation)'),
28
+ type: z
29
+ .enum(['BLOCKS', 'IS_BLOCKED_BY', 'RELATES_TO'])
30
+ .optional()
31
+ .describe('Dependency type (required for create operation)'),
32
+ },
33
+ async (params) => {
34
+ try {
35
+ const { operation } = params;
36
+
37
+ // Handle create operation
38
+ if (operation === 'create') {
39
+ const { fromTaskId, toTaskId, type } = params;
40
+
41
+ // Validate required parameters for create
42
+ if (!fromTaskId) {
43
+ return {
44
+ content: [
45
+ {
46
+ type: 'text' as const,
47
+ text: JSON.stringify(
48
+ createErrorResponse('fromTaskId is required for create operation'),
49
+ null,
50
+ 2
51
+ ),
52
+ },
53
+ ],
54
+ };
55
+ }
56
+
57
+ if (!toTaskId) {
58
+ return {
59
+ content: [
60
+ {
61
+ type: 'text' as const,
62
+ text: JSON.stringify(
63
+ createErrorResponse('toTaskId is required for create operation'),
64
+ null,
65
+ 2
66
+ ),
67
+ },
68
+ ],
69
+ };
70
+ }
71
+
72
+ if (!type) {
73
+ return {
74
+ content: [
75
+ {
76
+ type: 'text' as const,
77
+ text: JSON.stringify(
78
+ createErrorResponse('type is required for create operation'),
79
+ null,
80
+ 2
81
+ ),
82
+ },
83
+ ],
84
+ };
85
+ }
86
+
87
+ const result = createDependency({
88
+ fromTaskId,
89
+ toTaskId,
90
+ type: type as DependencyType,
91
+ });
92
+
93
+ if (result.success === false) {
94
+ return {
95
+ content: [
96
+ {
97
+ type: 'text' as const,
98
+ text: JSON.stringify(
99
+ createErrorResponse(result.error, result.code),
100
+ null,
101
+ 2
102
+ ),
103
+ },
104
+ ],
105
+ };
106
+ }
107
+
108
+ return {
109
+ content: [
110
+ {
111
+ type: 'text' as const,
112
+ text: JSON.stringify(
113
+ createSuccessResponse('Dependency created', result.data),
114
+ null,
115
+ 2
116
+ ),
117
+ },
118
+ ],
119
+ };
120
+ }
121
+
122
+ // Handle delete operation
123
+ if (operation === 'delete') {
124
+ const { id } = params;
125
+
126
+ // Validate required parameters for delete
127
+ if (!id) {
128
+ return {
129
+ content: [
130
+ {
131
+ type: 'text' as const,
132
+ text: JSON.stringify(
133
+ createErrorResponse('id is required for delete operation'),
134
+ null,
135
+ 2
136
+ ),
137
+ },
138
+ ],
139
+ };
140
+ }
141
+
142
+ const result = deleteDependency(id);
143
+
144
+ if (result.success === false) {
145
+ return {
146
+ content: [
147
+ {
148
+ type: 'text' as const,
149
+ text: JSON.stringify(
150
+ createErrorResponse(result.error, result.code),
151
+ null,
152
+ 2
153
+ ),
154
+ },
155
+ ],
156
+ };
157
+ }
158
+
159
+ return {
160
+ content: [
161
+ {
162
+ type: 'text' as const,
163
+ text: JSON.stringify(
164
+ createSuccessResponse('Dependency deleted', { deleted: true }),
165
+ null,
166
+ 2
167
+ ),
168
+ },
169
+ ],
170
+ };
171
+ }
172
+
173
+ // Invalid operation (should never happen due to enum validation)
174
+ return {
175
+ content: [
176
+ {
177
+ type: 'text' as const,
178
+ text: JSON.stringify(
179
+ createErrorResponse(`Invalid operation: ${operation}`),
180
+ null,
181
+ 2
182
+ ),
183
+ },
184
+ ],
185
+ };
186
+ } catch (error: any) {
187
+ return {
188
+ content: [
189
+ {
190
+ type: 'text' as const,
191
+ text: JSON.stringify(createErrorResponse(error.message), null, 2),
192
+ },
193
+ ],
194
+ };
195
+ }
196
+ }
197
+ );
198
+ }