mcp-product-manager
Version:
MCP Orchestrator for task and project management with web interface
166 lines • 6.11 kB
JavaScript
// create.js - POST /api/bundles
import { Router } from 'express';
import { run, get, query } from '../../utils/database.js';
import { success, error, asyncHandler } from '../../utils/response.js';
const router = Router();
// Create a bundle from task IDs
router.post('/api/bundles', asyncHandler(async (req, res) => {
const { project, bundle_type = 'shared_files', task_ids, // Array or comma-separated string
reason, created_by = 'orchestrator' } = req.body;
if (!project || !task_ids || !reason) {
const errorResponse = error('Missing required fields: project, task_ids, reason', 400);
return res.status(errorResponse.status).json(errorResponse.body);
}
try {
// Convert to array if string
const taskIdsArray = Array.isArray(task_ids)
? task_ids
: task_ids.split(',').map(id => id.trim());
// Validate at least 2 tasks
if (taskIdsArray.length < 2) {
const errorResponse = error('Bundle must contain at least 2 tasks', 400);
return res.status(errorResponse.status).json(errorResponse.body);
}
// Verify all tasks exist and belong to project
const placeholders = taskIdsArray.map(() => '?').join(',');
const tasksQuery = `
SELECT id, estimated_hours, status, description, priority
FROM tasks
WHERE project = ?
AND id IN (${placeholders})
`;
const tasks = await query(tasksQuery, [project, ...taskIdsArray]);
if (tasks.length !== taskIdsArray.length) {
const foundIds = tasks.map(t => t.id);
const missingIds = taskIdsArray.filter(id => !foundIds.includes(id));
const errorResponse = error(`Tasks not found: ${missingIds.join(', ')}`, 404);
return res.status(errorResponse.status).json(errorResponse.body);
}
// Calculate total hours
const totalHours = tasks.reduce((sum, task) => sum + (task.estimated_hours || 0), 0);
// Generate bundle ID
const bundleId = `BUNDLE-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`;
// Begin transaction
await run('BEGIN TRANSACTION');
try {
// Create bundle
await run(`
INSERT INTO task_bundles (
id, project, bundle_type, task_count,
total_estimated_hours, status, bundle_reason, created_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, [
bundleId,
project,
bundle_type,
taskIdsArray.length,
totalHours,
'proposed',
reason,
created_by
]);
// Link tasks to bundle with position
const bundleTaskInserts = taskIdsArray.map((taskId, index) => run('INSERT INTO bundle_tasks (bundle_id, task_id, position) VALUES (?, ?, ?)', [bundleId, taskId, index + 1]));
await Promise.all(bundleTaskInserts);
// Log creation
await run(`
INSERT INTO coordination_updates (
action,
agent_id,
task_id,
data
) VALUES (
'bundle_created',
?,
?,
?
)
`, [created_by, bundleId, JSON.stringify({
bundle_type,
reason,
task_count: taskIdsArray.length,
total_hours: totalHours
})]);
await run('COMMIT');
// Get full bundle details
const bundleQuery = `
SELECT
b.*,
json_group_array(
json_object(
'id', t.id,
'description', t.description,
'status', t.status,
'priority', t.priority,
'estimated_hours', t.estimated_hours
)
) as tasks
FROM task_bundles b
JOIN bundle_tasks bt ON b.id = bt.bundle_id
JOIN tasks t ON bt.task_id = t.id
WHERE b.id = ?
GROUP BY b.id
`;
const bundle = await get(bundleQuery, [bundleId]);
res.status(201).json(success({
bundle_id: bundleId,
bundle: {
...bundle,
tasks: JSON.parse(bundle.tasks)
}
}, 'Bundle created successfully'));
}
catch (err) {
await run('ROLLBACK');
throw err;
}
}
catch (err) {
console.error('Failed to create bundle:', err);
const errorResponse = error('Failed to create bundle', 500, {
error: err.message
});
res.status(errorResponse.status).json(errorResponse.body);
}
}));
// MCP tool definition
export const tool = {
name: 'bundle_create',
description: 'Create a bundle from multiple task IDs for efficient batch processing',
inputSchema: {
type: 'object',
properties: {
project: {
type: 'string',
description: 'Project name'
},
task_ids: {
type: 'array',
items: {
type: 'string'
},
description: 'Array of task IDs to bundle together',
minItems: 2
},
bundle_type: {
type: 'string',
enum: ['shared_files', 'sequential', 'related', 'manual'],
description: 'Type of bundle',
default: 'shared_files'
},
reason: {
type: 'string',
description: 'Reason for creating this bundle'
},
created_by: {
type: 'string',
description: 'Who is creating this bundle',
default: 'orchestrator'
}
},
required: ['project', 'task_ids', 'reason']
}
};
export { router };
export default router;
//# sourceMappingURL=create.js.map