@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.
- package/README.md +15 -0
- package/package.json +51 -0
- package/src/db/client.ts +34 -0
- package/src/db/index.ts +1 -0
- package/src/db/migrate.ts +51 -0
- package/src/db/migrations/001_initial_schema.sql +160 -0
- package/src/domain/index.ts +1 -0
- package/src/domain/types.ts +225 -0
- package/src/index.ts +7 -0
- package/src/repos/base.ts +151 -0
- package/src/repos/dependencies.ts +356 -0
- package/src/repos/features.ts +507 -0
- package/src/repos/index.ts +4 -0
- package/src/repos/projects.ts +350 -0
- package/src/repos/sections.ts +505 -0
- package/src/repos/tags.example.ts +125 -0
- package/src/repos/tags.ts +175 -0
- package/src/repos/tasks.ts +581 -0
- package/src/repos/templates.ts +649 -0
- package/src/server.ts +121 -0
- package/src/services/index.ts +2 -0
- package/src/services/status-validator.ts +100 -0
- package/src/services/workflow.ts +104 -0
- package/src/tools/apply-template.ts +129 -0
- package/src/tools/get-blocked-tasks.ts +63 -0
- package/src/tools/get-next-status.ts +183 -0
- package/src/tools/get-next-task.ts +75 -0
- package/src/tools/get-tag-usage.ts +54 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/list-tags.ts +56 -0
- package/src/tools/manage-container.ts +333 -0
- package/src/tools/manage-dependency.ts +198 -0
- package/src/tools/manage-sections.ts +388 -0
- package/src/tools/manage-template.ts +313 -0
- package/src/tools/query-container.ts +296 -0
- package/src/tools/query-dependencies.ts +68 -0
- package/src/tools/query-sections.ts +70 -0
- package/src/tools/query-templates.ts +137 -0
- package/src/tools/query-workflow-state.ts +198 -0
- package/src/tools/registry.ts +180 -0
- package/src/tools/rename-tag.ts +64 -0
- package/src/tools/setup-project.ts +189 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createSuccessResponse, createErrorResponse, uuidSchema } from './registry';
|
|
4
|
+
import { getProject } from '../repos/projects';
|
|
5
|
+
import { getFeature } from '../repos/features';
|
|
6
|
+
import { getTask } from '../repos/tasks';
|
|
7
|
+
import { getDependencies } from '../repos/dependencies';
|
|
8
|
+
import { ProjectStatus, FeatureStatus, TaskStatus } from '../domain/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Status transition maps (same as get-next-status.ts)
|
|
12
|
+
*
|
|
13
|
+
* Note: CANCELLED and DEFERRED are intentionally non-terminal statuses.
|
|
14
|
+
* They allow transitions back to earlier workflow stages (BACKLOG/PENDING for tasks,
|
|
15
|
+
* PLANNING for projects) to support reinstating cancelled or deferred work.
|
|
16
|
+
*/
|
|
17
|
+
const PROJECT_TRANSITIONS: Record<ProjectStatus, ProjectStatus[]> = {
|
|
18
|
+
[ProjectStatus.PLANNING]: [ProjectStatus.IN_DEVELOPMENT, ProjectStatus.ON_HOLD, ProjectStatus.CANCELLED],
|
|
19
|
+
[ProjectStatus.IN_DEVELOPMENT]: [ProjectStatus.COMPLETED, ProjectStatus.ON_HOLD, ProjectStatus.CANCELLED],
|
|
20
|
+
[ProjectStatus.ON_HOLD]: [ProjectStatus.PLANNING, ProjectStatus.IN_DEVELOPMENT, ProjectStatus.CANCELLED],
|
|
21
|
+
[ProjectStatus.COMPLETED]: [ProjectStatus.ARCHIVED],
|
|
22
|
+
[ProjectStatus.CANCELLED]: [ProjectStatus.PLANNING], // Non-terminal: allows reinstating cancelled projects
|
|
23
|
+
[ProjectStatus.ARCHIVED]: []
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const FEATURE_TRANSITIONS: Record<FeatureStatus, FeatureStatus[]> = {
|
|
27
|
+
[FeatureStatus.DRAFT]: [FeatureStatus.PLANNING],
|
|
28
|
+
[FeatureStatus.PLANNING]: [FeatureStatus.IN_DEVELOPMENT, FeatureStatus.ON_HOLD],
|
|
29
|
+
[FeatureStatus.IN_DEVELOPMENT]: [FeatureStatus.TESTING, FeatureStatus.BLOCKED, FeatureStatus.ON_HOLD],
|
|
30
|
+
[FeatureStatus.TESTING]: [FeatureStatus.VALIDATING, FeatureStatus.IN_DEVELOPMENT],
|
|
31
|
+
[FeatureStatus.VALIDATING]: [FeatureStatus.PENDING_REVIEW, FeatureStatus.IN_DEVELOPMENT],
|
|
32
|
+
[FeatureStatus.PENDING_REVIEW]: [FeatureStatus.DEPLOYED, FeatureStatus.IN_DEVELOPMENT],
|
|
33
|
+
[FeatureStatus.BLOCKED]: [FeatureStatus.IN_DEVELOPMENT, FeatureStatus.ON_HOLD],
|
|
34
|
+
[FeatureStatus.ON_HOLD]: [FeatureStatus.PLANNING, FeatureStatus.IN_DEVELOPMENT],
|
|
35
|
+
[FeatureStatus.DEPLOYED]: [FeatureStatus.COMPLETED],
|
|
36
|
+
[FeatureStatus.COMPLETED]: [FeatureStatus.ARCHIVED],
|
|
37
|
+
[FeatureStatus.ARCHIVED]: []
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const TASK_TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {
|
|
41
|
+
[TaskStatus.BACKLOG]: [TaskStatus.PENDING],
|
|
42
|
+
[TaskStatus.PENDING]: [TaskStatus.IN_PROGRESS, TaskStatus.BLOCKED, TaskStatus.ON_HOLD, TaskStatus.CANCELLED, TaskStatus.DEFERRED],
|
|
43
|
+
[TaskStatus.IN_PROGRESS]: [TaskStatus.IN_REVIEW, TaskStatus.TESTING, TaskStatus.BLOCKED, TaskStatus.ON_HOLD, TaskStatus.COMPLETED],
|
|
44
|
+
[TaskStatus.IN_REVIEW]: [TaskStatus.CHANGES_REQUESTED, TaskStatus.COMPLETED],
|
|
45
|
+
[TaskStatus.CHANGES_REQUESTED]: [TaskStatus.IN_PROGRESS],
|
|
46
|
+
[TaskStatus.TESTING]: [TaskStatus.READY_FOR_QA, TaskStatus.IN_PROGRESS],
|
|
47
|
+
[TaskStatus.READY_FOR_QA]: [TaskStatus.INVESTIGATING, TaskStatus.DEPLOYED, TaskStatus.COMPLETED],
|
|
48
|
+
[TaskStatus.INVESTIGATING]: [TaskStatus.IN_PROGRESS, TaskStatus.BLOCKED],
|
|
49
|
+
[TaskStatus.BLOCKED]: [TaskStatus.PENDING, TaskStatus.IN_PROGRESS],
|
|
50
|
+
[TaskStatus.ON_HOLD]: [TaskStatus.PENDING, TaskStatus.IN_PROGRESS],
|
|
51
|
+
[TaskStatus.DEPLOYED]: [TaskStatus.COMPLETED],
|
|
52
|
+
[TaskStatus.COMPLETED]: [], // Terminal: no transitions allowed
|
|
53
|
+
[TaskStatus.CANCELLED]: [TaskStatus.BACKLOG, TaskStatus.PENDING], // Non-terminal: allows reinstating cancelled tasks
|
|
54
|
+
[TaskStatus.DEFERRED]: [TaskStatus.BACKLOG, TaskStatus.PENDING] // Non-terminal: allows resuming deferred tasks
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Register the query_workflow_state MCP tool.
|
|
59
|
+
*
|
|
60
|
+
* Returns the full workflow state for a container including current status,
|
|
61
|
+
* allowed transitions, and dependency information for tasks.
|
|
62
|
+
*
|
|
63
|
+
* Note: CANCELLED and DEFERRED statuses can transition back to earlier stages
|
|
64
|
+
* (BACKLOG/PENDING for tasks, PLANNING for projects) to support work reinstatement.
|
|
65
|
+
*/
|
|
66
|
+
export function registerQueryWorkflowStateTool(server: McpServer): void {
|
|
67
|
+
server.tool(
|
|
68
|
+
'query_workflow_state',
|
|
69
|
+
'Query the full workflow state for a container. Returns current status, allowed transitions, whether the status is terminal, and for tasks also includes dependency information (blocking/blocked-by tasks and whether all blockers are resolved).',
|
|
70
|
+
{
|
|
71
|
+
containerType: z.enum(['project', 'feature', 'task']).describe('Type of container (project, feature, or task)'),
|
|
72
|
+
id: uuidSchema.describe('ID of the container')
|
|
73
|
+
},
|
|
74
|
+
async (params: any) => {
|
|
75
|
+
try {
|
|
76
|
+
const { containerType, id } = params;
|
|
77
|
+
|
|
78
|
+
let currentStatus: string;
|
|
79
|
+
let allowedTransitions: string[] = [];
|
|
80
|
+
let dependencies: any = undefined;
|
|
81
|
+
|
|
82
|
+
switch (containerType) {
|
|
83
|
+
case 'project': {
|
|
84
|
+
const result = getProject(id);
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
return {
|
|
87
|
+
content: [{
|
|
88
|
+
type: 'text' as const,
|
|
89
|
+
text: JSON.stringify(createErrorResponse(result.error, result.code), null, 2)
|
|
90
|
+
}]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
currentStatus = result.data.status;
|
|
95
|
+
allowedTransitions = PROJECT_TRANSITIONS[currentStatus as ProjectStatus] || [];
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case 'feature': {
|
|
100
|
+
const result = getFeature(id);
|
|
101
|
+
if (!result.success) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{
|
|
104
|
+
type: 'text' as const,
|
|
105
|
+
text: JSON.stringify(createErrorResponse(result.error, result.code), null, 2)
|
|
106
|
+
}]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
currentStatus = result.data.status;
|
|
111
|
+
allowedTransitions = FEATURE_TRANSITIONS[currentStatus as FeatureStatus] || [];
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'task': {
|
|
116
|
+
const taskResult = getTask(id);
|
|
117
|
+
if (!taskResult.success) {
|
|
118
|
+
return {
|
|
119
|
+
content: [{
|
|
120
|
+
type: 'text' as const,
|
|
121
|
+
text: JSON.stringify(createErrorResponse(taskResult.error, taskResult.code), null, 2)
|
|
122
|
+
}]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
currentStatus = taskResult.data.status;
|
|
127
|
+
allowedTransitions = TASK_TRANSITIONS[currentStatus as TaskStatus] || [];
|
|
128
|
+
|
|
129
|
+
// Get dependencies for tasks
|
|
130
|
+
const depsResult = getDependencies(id, 'both');
|
|
131
|
+
if (depsResult.success) {
|
|
132
|
+
const blocking = depsResult.data.filter(d => d.fromTaskId === id && d.type === 'BLOCKS');
|
|
133
|
+
const blockedBy = depsResult.data.filter(d => d.toTaskId === id && d.type === 'BLOCKS');
|
|
134
|
+
|
|
135
|
+
// Check if all blockers are resolved
|
|
136
|
+
const allBlockersResolved = blockedBy.length === 0 || blockedBy.every(dep => {
|
|
137
|
+
const blockerResult = getTask(dep.fromTaskId);
|
|
138
|
+
if (!blockerResult.success) return false;
|
|
139
|
+
const blockerStatus = blockerResult.data.status;
|
|
140
|
+
return blockerStatus === TaskStatus.COMPLETED || blockerStatus === TaskStatus.CANCELLED;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
dependencies = {
|
|
144
|
+
blocking: blocking.map(d => d.toTaskId),
|
|
145
|
+
blockedBy: blockedBy.map(d => d.fromTaskId),
|
|
146
|
+
allBlockersResolved
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
default:
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: 'text' as const,
|
|
156
|
+
text: JSON.stringify(
|
|
157
|
+
createErrorResponse(`Invalid container type: ${containerType}`, 'VALIDATION_ERROR'),
|
|
158
|
+
null,
|
|
159
|
+
2
|
|
160
|
+
)
|
|
161
|
+
}]
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const workflowState = {
|
|
166
|
+
id,
|
|
167
|
+
containerType,
|
|
168
|
+
currentStatus,
|
|
169
|
+
allowedTransitions,
|
|
170
|
+
isTerminal: allowedTransitions.length === 0,
|
|
171
|
+
...(dependencies && { dependencies })
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
content: [{
|
|
176
|
+
type: 'text' as const,
|
|
177
|
+
text: JSON.stringify(
|
|
178
|
+
createSuccessResponse('Workflow state retrieved successfully', workflowState),
|
|
179
|
+
null,
|
|
180
|
+
2
|
|
181
|
+
)
|
|
182
|
+
}]
|
|
183
|
+
};
|
|
184
|
+
} catch (error: any) {
|
|
185
|
+
return {
|
|
186
|
+
content: [{
|
|
187
|
+
type: 'text' as const,
|
|
188
|
+
text: JSON.stringify(
|
|
189
|
+
createErrorResponse('Failed to query workflow state', error.message),
|
|
190
|
+
null,
|
|
191
|
+
2
|
|
192
|
+
)
|
|
193
|
+
}]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
// --- Shared UUID schemas ---
|
|
5
|
+
// These schemas accept standard dashed UUIDs and transform them to dashless format
|
|
6
|
+
// to match the storage format in the database
|
|
7
|
+
export const uuidSchema = z.string().uuid().transform(v => v.replace(/-/g, '').toLowerCase());
|
|
8
|
+
export const optionalUuidSchema = z.string().uuid().optional().transform(v => v ? v.replace(/-/g, '').toLowerCase() : undefined);
|
|
9
|
+
|
|
10
|
+
// --- Tool Definition interface ---
|
|
11
|
+
export interface ToolDefinition {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
parameters: Record<string, any>; // Zod schemas
|
|
15
|
+
execute: (params: Record<string, any>) => Promise<ToolResponse>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// --- Standard response format ---
|
|
19
|
+
export interface ToolResponse {
|
|
20
|
+
success: boolean;
|
|
21
|
+
message: string;
|
|
22
|
+
data?: any;
|
|
23
|
+
error?: string;
|
|
24
|
+
metadata?: {
|
|
25
|
+
timestamp: string;
|
|
26
|
+
version: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createSuccessResponse(message: string, data: any): ToolResponse {
|
|
31
|
+
return {
|
|
32
|
+
success: true,
|
|
33
|
+
message,
|
|
34
|
+
data,
|
|
35
|
+
metadata: {
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
version: '1.0.0'
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createErrorResponse(message: string, error?: string): ToolResponse {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
message,
|
|
46
|
+
error: error || message,
|
|
47
|
+
metadata: {
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
version: '1.0.0'
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Parameter validation helpers ---
|
|
55
|
+
|
|
56
|
+
export function requireString(params: Record<string, any>, key: string): string {
|
|
57
|
+
const value = params[key];
|
|
58
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
59
|
+
throw new Error(`Required string parameter '${key}' is missing or empty`);
|
|
60
|
+
}
|
|
61
|
+
return value.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function optionalString(params: Record<string, any>, key: string): string | undefined {
|
|
65
|
+
const value = params[key];
|
|
66
|
+
if (value === undefined || value === null) return undefined;
|
|
67
|
+
if (typeof value !== 'string') throw new Error(`Parameter '${key}' must be a string`);
|
|
68
|
+
return value.trim() || undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function requireUUID(params: Record<string, any>, key: string): string {
|
|
72
|
+
const value = requireString(params, key);
|
|
73
|
+
// Accept both dashed and non-dashed UUID formats
|
|
74
|
+
const uuidRegex = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
|
|
75
|
+
if (!uuidRegex.test(value)) {
|
|
76
|
+
throw new Error(`Parameter '${key}' must be a valid UUID`);
|
|
77
|
+
}
|
|
78
|
+
return value.replace(/-/g, '').toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function optionalUUID(params: Record<string, any>, key: string): string | undefined {
|
|
82
|
+
const value = params[key];
|
|
83
|
+
if (value === undefined || value === null) return undefined;
|
|
84
|
+
return requireUUID(params, key);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function requireBoolean(params: Record<string, any>, key: string): boolean {
|
|
88
|
+
const value = params[key];
|
|
89
|
+
if (typeof value !== 'boolean') {
|
|
90
|
+
throw new Error(`Required boolean parameter '${key}' is missing`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function optionalBoolean(params: Record<string, any>, key: string, defaultValue?: boolean): boolean | undefined {
|
|
96
|
+
const value = params[key];
|
|
97
|
+
if (value === undefined || value === null) return defaultValue;
|
|
98
|
+
if (typeof value !== 'boolean') throw new Error(`Parameter '${key}' must be a boolean`);
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function requireInteger(params: Record<string, any>, key: string): number {
|
|
103
|
+
const value = params[key];
|
|
104
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
105
|
+
throw new Error(`Required integer parameter '${key}' is missing or not an integer`);
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function optionalInteger(params: Record<string, any>, key: string, defaultValue?: number): number | undefined {
|
|
111
|
+
const value = params[key];
|
|
112
|
+
if (value === undefined || value === null) return defaultValue;
|
|
113
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
114
|
+
throw new Error(`Parameter '${key}' must be an integer`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function requireEnum<T extends string>(params: Record<string, any>, key: string, validValues: readonly T[]): T {
|
|
120
|
+
const value = requireString(params, key).toUpperCase() as T;
|
|
121
|
+
if (!validValues.includes(value)) {
|
|
122
|
+
throw new Error(`Parameter '${key}' must be one of: ${validValues.join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function optionalEnum<T extends string>(params: Record<string, any>, key: string, validValues: readonly T[]): T | undefined {
|
|
128
|
+
const value = optionalString(params, key);
|
|
129
|
+
if (!value) return undefined;
|
|
130
|
+
const upper = value.toUpperCase() as T;
|
|
131
|
+
if (!validValues.includes(upper)) {
|
|
132
|
+
throw new Error(`Parameter '${key}' must be one of: ${validValues.join(', ')}`);
|
|
133
|
+
}
|
|
134
|
+
return upper;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- Error codes ---
|
|
138
|
+
export enum ErrorCode {
|
|
139
|
+
NOT_FOUND = 'NOT_FOUND',
|
|
140
|
+
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
|
141
|
+
CONFLICT = 'CONFLICT',
|
|
142
|
+
INTERNAL_ERROR = 'INTERNAL_ERROR',
|
|
143
|
+
INVALID_OPERATION = 'INVALID_OPERATION',
|
|
144
|
+
CIRCULAR_DEPENDENCY = 'CIRCULAR_DEPENDENCY',
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Tool registration helper ---
|
|
148
|
+
// This will be used to register tools with the MCP server instance
|
|
149
|
+
export function registerTool(server: McpServer, tool: ToolDefinition): void {
|
|
150
|
+
server.tool(
|
|
151
|
+
tool.name,
|
|
152
|
+
tool.description,
|
|
153
|
+
tool.parameters,
|
|
154
|
+
async (params: any) => {
|
|
155
|
+
try {
|
|
156
|
+
const response = await tool.execute(params);
|
|
157
|
+
return {
|
|
158
|
+
content: [{
|
|
159
|
+
type: 'text' as const,
|
|
160
|
+
text: JSON.stringify(response, null, 2)
|
|
161
|
+
}]
|
|
162
|
+
};
|
|
163
|
+
} catch (error: any) {
|
|
164
|
+
const response = createErrorResponse(
|
|
165
|
+
error.message || 'Internal error',
|
|
166
|
+
error.name === 'NotFoundError' ? ErrorCode.NOT_FOUND
|
|
167
|
+
: error.name === 'ValidationError' ? ErrorCode.VALIDATION_ERROR
|
|
168
|
+
: error.name === 'ConflictError' ? ErrorCode.CONFLICT
|
|
169
|
+
: ErrorCode.INTERNAL_ERROR
|
|
170
|
+
);
|
|
171
|
+
return {
|
|
172
|
+
content: [{
|
|
173
|
+
type: 'text' as const,
|
|
174
|
+
text: JSON.stringify(response, null, 2)
|
|
175
|
+
}]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createSuccessResponse, createErrorResponse } from './registry';
|
|
4
|
+
import { renameTag } from '../repos/tags';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register the rename_tag MCP tool
|
|
8
|
+
* Renames a tag across all entities, with optional dry-run mode
|
|
9
|
+
*/
|
|
10
|
+
export function registerRenameTagTool(server: McpServer): void {
|
|
11
|
+
server.tool(
|
|
12
|
+
'rename_tag',
|
|
13
|
+
'Rename a tag across all entities',
|
|
14
|
+
{
|
|
15
|
+
oldTag: z.string().describe('Current tag name'),
|
|
16
|
+
newTag: z.string().describe('New tag name'),
|
|
17
|
+
dryRun: z.boolean().optional().default(false).describe('Preview changes without applying them')
|
|
18
|
+
},
|
|
19
|
+
async (params) => {
|
|
20
|
+
try {
|
|
21
|
+
const result = renameTag(
|
|
22
|
+
params.oldTag,
|
|
23
|
+
params.newTag,
|
|
24
|
+
{ dryRun: params.dryRun ?? false }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: 'text' as const,
|
|
31
|
+
text: JSON.stringify(createErrorResponse(result.error), null, 2)
|
|
32
|
+
}]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const message = params.dryRun
|
|
37
|
+
? `Dry run: Would affect ${result.data.affected} entities`
|
|
38
|
+
: `Tag renamed successfully, affected ${result.data.affected} entities`;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
content: [{
|
|
42
|
+
type: 'text' as const,
|
|
43
|
+
text: JSON.stringify(
|
|
44
|
+
createSuccessResponse(message, result.data),
|
|
45
|
+
null,
|
|
46
|
+
2
|
|
47
|
+
)
|
|
48
|
+
}]
|
|
49
|
+
};
|
|
50
|
+
} catch (error: any) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: 'text' as const,
|
|
54
|
+
text: JSON.stringify(
|
|
55
|
+
createErrorResponse(error.message || 'Failed to rename tag'),
|
|
56
|
+
null,
|
|
57
|
+
2
|
|
58
|
+
)
|
|
59
|
+
}]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createSuccessResponse, createErrorResponse } from './registry';
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG_YAML = `# Task Orchestrator Configuration
|
|
8
|
+
version: "2.0"
|
|
9
|
+
|
|
10
|
+
workflows:
|
|
11
|
+
project:
|
|
12
|
+
statuses:
|
|
13
|
+
- PLANNING
|
|
14
|
+
- IN_DEVELOPMENT
|
|
15
|
+
- ON_HOLD
|
|
16
|
+
- CANCELLED
|
|
17
|
+
- COMPLETED
|
|
18
|
+
- ARCHIVED
|
|
19
|
+
transitions:
|
|
20
|
+
PLANNING: [IN_DEVELOPMENT, ON_HOLD, CANCELLED]
|
|
21
|
+
IN_DEVELOPMENT: [COMPLETED, ON_HOLD, CANCELLED]
|
|
22
|
+
ON_HOLD: [PLANNING, IN_DEVELOPMENT, CANCELLED]
|
|
23
|
+
COMPLETED: [ARCHIVED]
|
|
24
|
+
CANCELLED: [PLANNING]
|
|
25
|
+
ARCHIVED: []
|
|
26
|
+
|
|
27
|
+
feature:
|
|
28
|
+
statuses:
|
|
29
|
+
- DRAFT
|
|
30
|
+
- PLANNING
|
|
31
|
+
- IN_DEVELOPMENT
|
|
32
|
+
- TESTING
|
|
33
|
+
- VALIDATING
|
|
34
|
+
- PENDING_REVIEW
|
|
35
|
+
- BLOCKED
|
|
36
|
+
- ON_HOLD
|
|
37
|
+
- DEPLOYED
|
|
38
|
+
- COMPLETED
|
|
39
|
+
- ARCHIVED
|
|
40
|
+
transitions:
|
|
41
|
+
DRAFT: [PLANNING]
|
|
42
|
+
PLANNING: [IN_DEVELOPMENT, ON_HOLD]
|
|
43
|
+
IN_DEVELOPMENT: [TESTING, BLOCKED, ON_HOLD]
|
|
44
|
+
TESTING: [VALIDATING, IN_DEVELOPMENT]
|
|
45
|
+
VALIDATING: [PENDING_REVIEW, IN_DEVELOPMENT]
|
|
46
|
+
PENDING_REVIEW: [DEPLOYED, IN_DEVELOPMENT]
|
|
47
|
+
BLOCKED: [IN_DEVELOPMENT, ON_HOLD]
|
|
48
|
+
ON_HOLD: [PLANNING, IN_DEVELOPMENT]
|
|
49
|
+
DEPLOYED: [COMPLETED]
|
|
50
|
+
COMPLETED: [ARCHIVED]
|
|
51
|
+
ARCHIVED: []
|
|
52
|
+
|
|
53
|
+
task:
|
|
54
|
+
statuses:
|
|
55
|
+
- BACKLOG
|
|
56
|
+
- PENDING
|
|
57
|
+
- IN_PROGRESS
|
|
58
|
+
- IN_REVIEW
|
|
59
|
+
- CHANGES_REQUESTED
|
|
60
|
+
- TESTING
|
|
61
|
+
- READY_FOR_QA
|
|
62
|
+
- INVESTIGATING
|
|
63
|
+
- BLOCKED
|
|
64
|
+
- ON_HOLD
|
|
65
|
+
- DEPLOYED
|
|
66
|
+
- COMPLETED
|
|
67
|
+
- CANCELLED
|
|
68
|
+
- DEFERRED
|
|
69
|
+
transitions:
|
|
70
|
+
BACKLOG: [PENDING]
|
|
71
|
+
PENDING: [IN_PROGRESS, BLOCKED, ON_HOLD, CANCELLED, DEFERRED]
|
|
72
|
+
IN_PROGRESS: [IN_REVIEW, TESTING, BLOCKED, ON_HOLD, COMPLETED]
|
|
73
|
+
IN_REVIEW: [CHANGES_REQUESTED, COMPLETED]
|
|
74
|
+
CHANGES_REQUESTED: [IN_PROGRESS]
|
|
75
|
+
TESTING: [READY_FOR_QA, IN_PROGRESS]
|
|
76
|
+
READY_FOR_QA: [INVESTIGATING, DEPLOYED, COMPLETED]
|
|
77
|
+
INVESTIGATING: [IN_PROGRESS, BLOCKED]
|
|
78
|
+
BLOCKED: [PENDING, IN_PROGRESS]
|
|
79
|
+
ON_HOLD: [PENDING, IN_PROGRESS]
|
|
80
|
+
DEPLOYED: [COMPLETED]
|
|
81
|
+
COMPLETED: []
|
|
82
|
+
CANCELLED: [BACKLOG, PENDING]
|
|
83
|
+
DEFERRED: [BACKLOG, PENDING]
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sets up a project with Task Orchestrator configuration
|
|
88
|
+
* Creates .taskorchestrator/config.yaml with default status workflows
|
|
89
|
+
* Idempotent - skips if configuration already exists
|
|
90
|
+
*/
|
|
91
|
+
export function registerSetupProjectTool(server: McpServer): void {
|
|
92
|
+
server.tool(
|
|
93
|
+
'setup_project',
|
|
94
|
+
'Set up Task Orchestrator configuration in a project. Creates .taskorchestrator/config.yaml with default workflow definitions for projects, features, and tasks. Idempotent - safely skips if already configured.',
|
|
95
|
+
{
|
|
96
|
+
projectPath: z
|
|
97
|
+
.string()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('Project directory path. Defaults to current working directory.'),
|
|
100
|
+
},
|
|
101
|
+
async (params: any) => {
|
|
102
|
+
try {
|
|
103
|
+
// Determine project path
|
|
104
|
+
const projectPath = params.projectPath || process.cwd();
|
|
105
|
+
const configDir = join(projectPath, '.taskorchestrator');
|
|
106
|
+
const configFilePath = join(configDir, 'config.yaml');
|
|
107
|
+
|
|
108
|
+
// Check if config already exists
|
|
109
|
+
if (existsSync(configFilePath)) {
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: 'text' as const,
|
|
114
|
+
text: JSON.stringify(
|
|
115
|
+
createSuccessResponse(
|
|
116
|
+
'Project already configured',
|
|
117
|
+
{
|
|
118
|
+
path: configFilePath,
|
|
119
|
+
alreadyExists: true,
|
|
120
|
+
message: 'Configuration file already exists. No changes made.',
|
|
121
|
+
}
|
|
122
|
+
),
|
|
123
|
+
null,
|
|
124
|
+
2
|
|
125
|
+
),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Create directory if it doesn't exist
|
|
132
|
+
if (!existsSync(configDir)) {
|
|
133
|
+
mkdirSync(configDir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Write default configuration
|
|
137
|
+
writeFileSync(configFilePath, DEFAULT_CONFIG_YAML, 'utf-8');
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: 'text' as const,
|
|
143
|
+
text: JSON.stringify(
|
|
144
|
+
createSuccessResponse('Project configured successfully', {
|
|
145
|
+
path: configFilePath,
|
|
146
|
+
created: true,
|
|
147
|
+
message:
|
|
148
|
+
'Created .taskorchestrator/config.yaml with default workflow definitions.',
|
|
149
|
+
workflows: {
|
|
150
|
+
project: {
|
|
151
|
+
statuses: 6,
|
|
152
|
+
description: 'Project lifecycle management',
|
|
153
|
+
},
|
|
154
|
+
feature: {
|
|
155
|
+
statuses: 11,
|
|
156
|
+
description: 'Feature development workflow',
|
|
157
|
+
},
|
|
158
|
+
task: {
|
|
159
|
+
statuses: 14,
|
|
160
|
+
description: 'Task tracking workflow',
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
null,
|
|
165
|
+
2
|
|
166
|
+
),
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
} catch (error: any) {
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: 'text' as const,
|
|
175
|
+
text: JSON.stringify(
|
|
176
|
+
createErrorResponse(
|
|
177
|
+
`Failed to set up project: ${error.message}`,
|
|
178
|
+
error.stack
|
|
179
|
+
),
|
|
180
|
+
null,
|
|
181
|
+
2
|
|
182
|
+
),
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
}
|