@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
package/src/server.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
4
|
+
import { runMigrations } from './db/migrate';
|
|
5
|
+
import {
|
|
6
|
+
registerQueryContainerTool,
|
|
7
|
+
registerManageContainerTool,
|
|
8
|
+
registerQuerySectionsTool,
|
|
9
|
+
registerManageSectionsTool,
|
|
10
|
+
registerQueryTemplatesTool,
|
|
11
|
+
registerManageTemplateTool,
|
|
12
|
+
registerApplyTemplateTool,
|
|
13
|
+
registerQueryDependenciesTool,
|
|
14
|
+
registerManageDependencyTool,
|
|
15
|
+
registerListTagsTool,
|
|
16
|
+
registerGetTagUsageTool,
|
|
17
|
+
registerRenameTagTool,
|
|
18
|
+
registerGetNextTaskTool,
|
|
19
|
+
registerGetBlockedTasksTool,
|
|
20
|
+
registerGetNextStatusTool,
|
|
21
|
+
registerQueryWorkflowStateTool,
|
|
22
|
+
registerSetupProjectTool,
|
|
23
|
+
} from './tools';
|
|
24
|
+
|
|
25
|
+
// Initialize database and run migrations
|
|
26
|
+
runMigrations();
|
|
27
|
+
|
|
28
|
+
// Create MCP server
|
|
29
|
+
const server = new McpServer({
|
|
30
|
+
name: 'task-orchestrator',
|
|
31
|
+
version: '2.0.0',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Register all tools
|
|
35
|
+
registerQueryContainerTool(server);
|
|
36
|
+
registerManageContainerTool(server);
|
|
37
|
+
registerQuerySectionsTool(server);
|
|
38
|
+
registerManageSectionsTool(server);
|
|
39
|
+
registerQueryTemplatesTool(server);
|
|
40
|
+
registerManageTemplateTool(server);
|
|
41
|
+
registerApplyTemplateTool(server);
|
|
42
|
+
registerQueryDependenciesTool(server);
|
|
43
|
+
registerManageDependencyTool(server);
|
|
44
|
+
registerListTagsTool(server);
|
|
45
|
+
registerGetTagUsageTool(server);
|
|
46
|
+
registerRenameTagTool(server);
|
|
47
|
+
registerGetNextTaskTool(server);
|
|
48
|
+
registerGetBlockedTasksTool(server);
|
|
49
|
+
registerGetNextStatusTool(server);
|
|
50
|
+
registerQueryWorkflowStateTool(server);
|
|
51
|
+
registerSetupProjectTool(server);
|
|
52
|
+
|
|
53
|
+
// Determine transport mode from CLI args or env
|
|
54
|
+
const useHttp = process.argv.includes('--http') || process.env.TRANSPORT === 'http';
|
|
55
|
+
|
|
56
|
+
if (useHttp) {
|
|
57
|
+
const port = parseInt(process.env.PORT || '3100', 10);
|
|
58
|
+
|
|
59
|
+
// Map of session ID -> transport for stateful sessions
|
|
60
|
+
const sessions = new Map<string, WebStandardStreamableHTTPServerTransport>();
|
|
61
|
+
|
|
62
|
+
Bun.serve({
|
|
63
|
+
port,
|
|
64
|
+
fetch: async (req: Request) => {
|
|
65
|
+
const url = new URL(req.url);
|
|
66
|
+
|
|
67
|
+
if (url.pathname !== '/mcp') {
|
|
68
|
+
return new Response('Not Found', { status: 404 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check for existing session
|
|
72
|
+
const sessionId = req.headers.get('mcp-session-id');
|
|
73
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
74
|
+
return sessions.get(sessionId)!.handleRequest(req);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// New session — create transport and connect a fresh server instance
|
|
78
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
79
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
80
|
+
onsessioninitialized: (id) => {
|
|
81
|
+
sessions.set(id, transport);
|
|
82
|
+
},
|
|
83
|
+
onsessionclosed: (id) => {
|
|
84
|
+
sessions.delete(id);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const sessionServer = new McpServer({
|
|
89
|
+
name: 'task-orchestrator',
|
|
90
|
+
version: '2.0.0',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
registerQueryContainerTool(sessionServer);
|
|
94
|
+
registerManageContainerTool(sessionServer);
|
|
95
|
+
registerQuerySectionsTool(sessionServer);
|
|
96
|
+
registerManageSectionsTool(sessionServer);
|
|
97
|
+
registerQueryTemplatesTool(sessionServer);
|
|
98
|
+
registerManageTemplateTool(sessionServer);
|
|
99
|
+
registerApplyTemplateTool(sessionServer);
|
|
100
|
+
registerQueryDependenciesTool(sessionServer);
|
|
101
|
+
registerManageDependencyTool(sessionServer);
|
|
102
|
+
registerListTagsTool(sessionServer);
|
|
103
|
+
registerGetTagUsageTool(sessionServer);
|
|
104
|
+
registerRenameTagTool(sessionServer);
|
|
105
|
+
registerGetNextTaskTool(sessionServer);
|
|
106
|
+
registerGetBlockedTasksTool(sessionServer);
|
|
107
|
+
registerGetNextStatusTool(sessionServer);
|
|
108
|
+
registerQueryWorkflowStateTool(sessionServer);
|
|
109
|
+
registerSetupProjectTool(sessionServer);
|
|
110
|
+
|
|
111
|
+
await sessionServer.connect(transport);
|
|
112
|
+
return transport.handleRequest(req);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.error(`Task Orchestrator MCP listening on http://localhost:${port}/mcp`);
|
|
117
|
+
} else {
|
|
118
|
+
// Default: stdio transport
|
|
119
|
+
const transport = new StdioServerTransport();
|
|
120
|
+
await server.connect(transport);
|
|
121
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ProjectStatus, FeatureStatus, TaskStatus } from '../domain/types';
|
|
2
|
+
|
|
3
|
+
// Valid statuses per container type
|
|
4
|
+
const PROJECT_STATUSES = Object.values(ProjectStatus);
|
|
5
|
+
const FEATURE_STATUSES = Object.values(FeatureStatus);
|
|
6
|
+
const TASK_STATUSES = Object.values(TaskStatus);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Status transition maps
|
|
10
|
+
*
|
|
11
|
+
* Note: CANCELLED and DEFERRED are intentionally non-terminal statuses.
|
|
12
|
+
* They allow transitions back to earlier workflow stages (BACKLOG/PENDING for tasks,
|
|
13
|
+
* PLANNING for projects) to support reinstating cancelled or deferred work.
|
|
14
|
+
*/
|
|
15
|
+
const PROJECT_TRANSITIONS: Record<string, string[]> = {
|
|
16
|
+
PLANNING: ['IN_DEVELOPMENT', 'ON_HOLD', 'CANCELLED'],
|
|
17
|
+
IN_DEVELOPMENT: ['COMPLETED', 'ON_HOLD', 'CANCELLED'],
|
|
18
|
+
ON_HOLD: ['PLANNING', 'IN_DEVELOPMENT', 'CANCELLED'],
|
|
19
|
+
COMPLETED: ['ARCHIVED'],
|
|
20
|
+
CANCELLED: ['PLANNING'], // Non-terminal: allows reinstating cancelled projects
|
|
21
|
+
ARCHIVED: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const FEATURE_TRANSITIONS: Record<string, string[]> = {
|
|
25
|
+
DRAFT: ['PLANNING'],
|
|
26
|
+
PLANNING: ['IN_DEVELOPMENT', 'ON_HOLD'],
|
|
27
|
+
IN_DEVELOPMENT: ['TESTING', 'BLOCKED', 'ON_HOLD'],
|
|
28
|
+
TESTING: ['VALIDATING', 'IN_DEVELOPMENT'],
|
|
29
|
+
VALIDATING: ['PENDING_REVIEW', 'IN_DEVELOPMENT'],
|
|
30
|
+
PENDING_REVIEW: ['DEPLOYED', 'IN_DEVELOPMENT'],
|
|
31
|
+
BLOCKED: ['IN_DEVELOPMENT', 'ON_HOLD'],
|
|
32
|
+
ON_HOLD: ['PLANNING', 'IN_DEVELOPMENT'],
|
|
33
|
+
DEPLOYED: ['COMPLETED'],
|
|
34
|
+
COMPLETED: ['ARCHIVED'],
|
|
35
|
+
ARCHIVED: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const TASK_TRANSITIONS: Record<string, string[]> = {
|
|
39
|
+
BACKLOG: ['PENDING'],
|
|
40
|
+
PENDING: ['IN_PROGRESS', 'BLOCKED', 'ON_HOLD', 'CANCELLED', 'DEFERRED'],
|
|
41
|
+
IN_PROGRESS: ['IN_REVIEW', 'TESTING', 'BLOCKED', 'ON_HOLD', 'COMPLETED'],
|
|
42
|
+
IN_REVIEW: ['CHANGES_REQUESTED', 'COMPLETED'],
|
|
43
|
+
CHANGES_REQUESTED: ['IN_PROGRESS'],
|
|
44
|
+
TESTING: ['READY_FOR_QA', 'IN_PROGRESS'],
|
|
45
|
+
READY_FOR_QA: ['INVESTIGATING', 'DEPLOYED', 'COMPLETED'],
|
|
46
|
+
INVESTIGATING: ['IN_PROGRESS', 'BLOCKED'],
|
|
47
|
+
BLOCKED: ['PENDING', 'IN_PROGRESS'],
|
|
48
|
+
ON_HOLD: ['PENDING', 'IN_PROGRESS'],
|
|
49
|
+
DEPLOYED: ['COMPLETED'],
|
|
50
|
+
COMPLETED: [], // Terminal: no transitions allowed
|
|
51
|
+
CANCELLED: ['BACKLOG', 'PENDING'], // Non-terminal: allows reinstating cancelled tasks
|
|
52
|
+
DEFERRED: ['BACKLOG', 'PENDING'], // Non-terminal: allows resuming deferred tasks
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Terminal statuses (no transitions out)
|
|
56
|
+
const TERMINAL_STATUSES: Record<string, string[]> = {
|
|
57
|
+
project: ['ARCHIVED'],
|
|
58
|
+
feature: ['ARCHIVED'],
|
|
59
|
+
task: ['COMPLETED'],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type ContainerType = 'project' | 'feature' | 'task';
|
|
63
|
+
|
|
64
|
+
export function isValidStatus(containerType: ContainerType, status: string): boolean {
|
|
65
|
+
switch (containerType) {
|
|
66
|
+
case 'project': return PROJECT_STATUSES.includes(status as ProjectStatus);
|
|
67
|
+
case 'feature': return FEATURE_STATUSES.includes(status as FeatureStatus);
|
|
68
|
+
case 'task': return TASK_STATUSES.includes(status as TaskStatus);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getValidStatuses(containerType: ContainerType): string[] {
|
|
73
|
+
switch (containerType) {
|
|
74
|
+
case 'project': return [...PROJECT_STATUSES];
|
|
75
|
+
case 'feature': return [...FEATURE_STATUSES];
|
|
76
|
+
case 'task': return [...TASK_STATUSES];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getTransitions(containerType: ContainerType): Record<string, string[]> {
|
|
81
|
+
switch (containerType) {
|
|
82
|
+
case 'project': return PROJECT_TRANSITIONS;
|
|
83
|
+
case 'feature': return FEATURE_TRANSITIONS;
|
|
84
|
+
case 'task': return TASK_TRANSITIONS;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getAllowedTransitions(containerType: ContainerType, currentStatus: string): string[] {
|
|
89
|
+
const transitions = getTransitions(containerType);
|
|
90
|
+
return transitions[currentStatus] || [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isValidTransition(containerType: ContainerType, from: string, to: string): boolean {
|
|
94
|
+
const allowed = getAllowedTransitions(containerType, from);
|
|
95
|
+
return allowed.includes(to);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isTerminalStatus(containerType: ContainerType, status: string): boolean {
|
|
99
|
+
return TERMINAL_STATUSES[containerType]?.includes(status) ?? false;
|
|
100
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ContainerType } from './status-validator';
|
|
2
|
+
import { getAllowedTransitions, isTerminalStatus, isValidStatus } from './status-validator';
|
|
3
|
+
import { getProject } from '../repos/projects';
|
|
4
|
+
import { getFeature } from '../repos/features';
|
|
5
|
+
import { getTask } from '../repos/tasks';
|
|
6
|
+
import { getDependencies } from '../repos/dependencies';
|
|
7
|
+
import { searchTasks } from '../repos/tasks';
|
|
8
|
+
import type { Result } from '../domain/types';
|
|
9
|
+
import { ok, err } from '../repos/base';
|
|
10
|
+
|
|
11
|
+
export interface WorkflowState {
|
|
12
|
+
containerType: ContainerType;
|
|
13
|
+
id: string;
|
|
14
|
+
currentStatus: string;
|
|
15
|
+
allowedTransitions: string[];
|
|
16
|
+
isTerminal: boolean;
|
|
17
|
+
cascadeEvents?: string[];
|
|
18
|
+
blockingDependencies?: Array<{
|
|
19
|
+
taskId: string;
|
|
20
|
+
taskTitle: string;
|
|
21
|
+
status: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getWorkflowState(containerType: ContainerType, id: string): Result<WorkflowState> {
|
|
26
|
+
// 1. Get entity
|
|
27
|
+
let currentStatus: string;
|
|
28
|
+
|
|
29
|
+
switch (containerType) {
|
|
30
|
+
case 'project': {
|
|
31
|
+
const result = getProject(id);
|
|
32
|
+
if (!result.success) return err(result.error, result.code);
|
|
33
|
+
currentStatus = result.data.status;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case 'feature': {
|
|
37
|
+
const result = getFeature(id);
|
|
38
|
+
if (!result.success) return err(result.error, result.code);
|
|
39
|
+
currentStatus = result.data.status;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'task': {
|
|
43
|
+
const result = getTask(id);
|
|
44
|
+
if (!result.success) return err(result.error, result.code);
|
|
45
|
+
currentStatus = result.data.status;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Get allowed transitions
|
|
51
|
+
const allowedTransitions = getAllowedTransitions(containerType, currentStatus);
|
|
52
|
+
const isTerminal = isTerminalStatus(containerType, currentStatus);
|
|
53
|
+
|
|
54
|
+
// 3. Build state
|
|
55
|
+
const state: WorkflowState = {
|
|
56
|
+
containerType,
|
|
57
|
+
id,
|
|
58
|
+
currentStatus,
|
|
59
|
+
allowedTransitions,
|
|
60
|
+
isTerminal,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// 4. For tasks, check blocking dependencies
|
|
64
|
+
if (containerType === 'task') {
|
|
65
|
+
const depsResult = getDependencies(id, 'dependents');
|
|
66
|
+
if (depsResult.success) {
|
|
67
|
+
const blockers = depsResult.data
|
|
68
|
+
.filter(d => d.type === 'BLOCKS')
|
|
69
|
+
.map(d => {
|
|
70
|
+
const taskResult = getTask(d.fromTaskId);
|
|
71
|
+
if (taskResult.success) {
|
|
72
|
+
return {
|
|
73
|
+
taskId: d.fromTaskId,
|
|
74
|
+
taskTitle: taskResult.data.title,
|
|
75
|
+
status: taskResult.data.status,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { taskId: d.fromTaskId, taskTitle: 'Unknown', status: 'UNKNOWN' };
|
|
79
|
+
})
|
|
80
|
+
.filter(b => b.status !== 'COMPLETED' && b.status !== 'CANCELLED');
|
|
81
|
+
|
|
82
|
+
if (blockers.length > 0) {
|
|
83
|
+
state.blockingDependencies = blockers;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 5. Detect cascade events for features/projects
|
|
89
|
+
if (containerType === 'feature') {
|
|
90
|
+
const tasksResult = searchTasks({ featureId: id });
|
|
91
|
+
if (tasksResult.success && tasksResult.data.length > 0) {
|
|
92
|
+
const events: string[] = [];
|
|
93
|
+
const allCompleted = tasksResult.data.every(t => t.status === 'COMPLETED' || t.status === 'CANCELLED');
|
|
94
|
+
const anyStarted = tasksResult.data.some(t => t.status !== 'PENDING' && t.status !== 'BACKLOG');
|
|
95
|
+
|
|
96
|
+
if (allCompleted) events.push('all_tasks_complete');
|
|
97
|
+
if (anyStarted) events.push('first_task_started');
|
|
98
|
+
|
|
99
|
+
if (events.length > 0) state.cascadeEvents = events;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return ok(state);
|
|
104
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createSuccessResponse, createErrorResponse, uuidSchema } from './registry';
|
|
4
|
+
import { applyTemplate } from '../repos/templates';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registers the `apply_template` MCP tool.
|
|
8
|
+
*
|
|
9
|
+
* Applies a template to an entity (PROJECT, FEATURE, or TASK),
|
|
10
|
+
* creating sections from the template's section definitions.
|
|
11
|
+
*/
|
|
12
|
+
export function registerApplyTemplateTool(server: McpServer): void {
|
|
13
|
+
server.tool(
|
|
14
|
+
'apply_template',
|
|
15
|
+
'Apply a template to an entity (PROJECT, FEATURE, or TASK), creating sections from the template',
|
|
16
|
+
{
|
|
17
|
+
templateId: uuidSchema,
|
|
18
|
+
entityType: z.enum(['PROJECT', 'FEATURE', 'TASK']),
|
|
19
|
+
entityId: uuidSchema
|
|
20
|
+
},
|
|
21
|
+
async (params: any) => {
|
|
22
|
+
try {
|
|
23
|
+
// Validate required parameters
|
|
24
|
+
if (!params.templateId) {
|
|
25
|
+
return {
|
|
26
|
+
content: [{
|
|
27
|
+
type: 'text' as const,
|
|
28
|
+
text: JSON.stringify(
|
|
29
|
+
createErrorResponse('Template ID is required'),
|
|
30
|
+
null,
|
|
31
|
+
2
|
|
32
|
+
)
|
|
33
|
+
}]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!params.entityType) {
|
|
38
|
+
return {
|
|
39
|
+
content: [{
|
|
40
|
+
type: 'text' as const,
|
|
41
|
+
text: JSON.stringify(
|
|
42
|
+
createErrorResponse('Entity type is required'),
|
|
43
|
+
null,
|
|
44
|
+
2
|
|
45
|
+
)
|
|
46
|
+
}]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!params.entityId) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: 'text' as const,
|
|
54
|
+
text: JSON.stringify(
|
|
55
|
+
createErrorResponse('Entity ID is required'),
|
|
56
|
+
null,
|
|
57
|
+
2
|
|
58
|
+
)
|
|
59
|
+
}]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Validate entity type
|
|
64
|
+
const validEntityTypes = ['PROJECT', 'FEATURE', 'TASK'];
|
|
65
|
+
if (!validEntityTypes.includes(params.entityType)) {
|
|
66
|
+
return {
|
|
67
|
+
content: [{
|
|
68
|
+
type: 'text' as const,
|
|
69
|
+
text: JSON.stringify(
|
|
70
|
+
createErrorResponse(
|
|
71
|
+
`Invalid entity type. Must be one of: ${validEntityTypes.join(', ')}`
|
|
72
|
+
),
|
|
73
|
+
null,
|
|
74
|
+
2
|
|
75
|
+
)
|
|
76
|
+
}]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = applyTemplate(
|
|
81
|
+
params.templateId,
|
|
82
|
+
params.entityType,
|
|
83
|
+
params.entityId
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
return {
|
|
88
|
+
content: [{
|
|
89
|
+
type: 'text' as const,
|
|
90
|
+
text: JSON.stringify(
|
|
91
|
+
createErrorResponse(result.error || 'Failed to apply template'),
|
|
92
|
+
null,
|
|
93
|
+
2
|
|
94
|
+
)
|
|
95
|
+
}]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: 'text' as const,
|
|
102
|
+
text: JSON.stringify(
|
|
103
|
+
createSuccessResponse(
|
|
104
|
+
`Template applied successfully. Created ${result.data.length} section(s)`,
|
|
105
|
+
result.data
|
|
106
|
+
),
|
|
107
|
+
null,
|
|
108
|
+
2
|
|
109
|
+
)
|
|
110
|
+
}]
|
|
111
|
+
};
|
|
112
|
+
} catch (error: any) {
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: 'text' as const,
|
|
116
|
+
text: JSON.stringify(
|
|
117
|
+
createErrorResponse(
|
|
118
|
+
'Internal error',
|
|
119
|
+
error.message || 'Unknown error occurred'
|
|
120
|
+
),
|
|
121
|
+
null,
|
|
122
|
+
2
|
|
123
|
+
)
|
|
124
|
+
}]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createSuccessResponse, createErrorResponse, optionalUuidSchema } from './registry';
|
|
4
|
+
import { getBlockedTasks } from '../repos/dependencies';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register the get_blocked_tasks MCP tool.
|
|
8
|
+
*
|
|
9
|
+
* Returns all blocked tasks, either with status 'BLOCKED' or tasks
|
|
10
|
+
* that have incomplete blocking dependencies.
|
|
11
|
+
*/
|
|
12
|
+
export function registerGetBlockedTasksTool(server: McpServer): void {
|
|
13
|
+
server.tool(
|
|
14
|
+
'get_blocked_tasks',
|
|
15
|
+
'Get all blocked tasks. Returns tasks that either have status BLOCKED or have incomplete blocking dependencies (tasks that block them but are not completed). Results are sorted by priority (descending) then creation time (ascending).',
|
|
16
|
+
{
|
|
17
|
+
projectId: optionalUuidSchema.describe('Filter by project ID'),
|
|
18
|
+
featureId: optionalUuidSchema.describe('Filter by feature ID')
|
|
19
|
+
},
|
|
20
|
+
async (params: any) => {
|
|
21
|
+
try {
|
|
22
|
+
const result = getBlockedTasks({
|
|
23
|
+
projectId: params.projectId,
|
|
24
|
+
featureId: params.featureId
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: 'text' as const,
|
|
31
|
+
text: JSON.stringify(createErrorResponse(result.error, result.code), null, 2)
|
|
32
|
+
}]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: 'text' as const,
|
|
39
|
+
text: JSON.stringify(
|
|
40
|
+
createSuccessResponse(
|
|
41
|
+
`Found ${result.data.length} blocked task(s)`,
|
|
42
|
+
result.data
|
|
43
|
+
),
|
|
44
|
+
null,
|
|
45
|
+
2
|
|
46
|
+
)
|
|
47
|
+
}]
|
|
48
|
+
};
|
|
49
|
+
} catch (error: any) {
|
|
50
|
+
return {
|
|
51
|
+
content: [{
|
|
52
|
+
type: 'text' as const,
|
|
53
|
+
text: JSON.stringify(
|
|
54
|
+
createErrorResponse('Failed to get blocked tasks', error.message),
|
|
55
|
+
null,
|
|
56
|
+
2
|
|
57
|
+
)
|
|
58
|
+
}]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|