Skip to main content
Glama
manage-project-workflow.ts17.7 kB
import { z } from 'zod'; import { createTool, withFormatParam } from '../base.js'; import { floatApi } from '../../services/float-api.js'; import { phaseSchema, phasesResponseSchema, milestoneSchema, milestonesResponseSchema, projectTaskSchema, projectTasksResponseSchema, allocationSchema, allocationsResponseSchema, } from '../../types/float.js'; // Workflow entity types enum for decision tree routing const workflowEntityTypeSchema = z.enum(['phases', 'milestones', 'project-tasks', 'allocations']); // Workflow operation types enum for decision tree routing const workflowOperationTypeSchema = z.enum([ 'list', 'get', 'create', 'update', 'delete', // Phase-specific operations 'list-phases-by-project', 'get-phases-by-date-range', 'get-active-phases', 'get-phase-schedule', // Milestone-specific operations 'get-project-milestones', 'get-upcoming-milestones', 'get-overdue-milestones', 'complete-milestone', 'get-milestone-reminders', // Project task-specific operations 'get-project-tasks-by-project', 'get-project-tasks-by-phase', 'bulk-create-project-tasks', 'reorder-project-tasks', 'archive-project-task', 'get-project-task-dependencies', ]); // Base parameters for workflow operations const workflowBaseParamsSchema = z.object({ entity_type: workflowEntityTypeSchema.describe( 'The type of workflow entity (phases, milestones, project-tasks, allocations)' ), operation: workflowOperationTypeSchema.describe('The workflow operation to perform'), }); // Generic list/filter parameters const workflowListParamsSchema = z .object({ project_id: z.union([z.string(), z.number()]).optional().describe('Filter by project ID'), phase_id: z.union([z.string(), z.number()]).optional().describe('Filter by phase ID'), people_id: z.union([z.string(), z.number()]).optional().describe('Filter by person ID'), start_date: z.string().optional().describe('Filter by start date (YYYY-MM-DD)'), end_date: z.string().optional().describe('Filter by end date (YYYY-MM-DD)'), status: z.union([z.string(), z.number()]).optional().describe('Filter by status'), active: z.number().optional().describe('Filter by active status (0=archived, 1=active)'), page: z.number().optional().describe('Page number for pagination'), 'per-page': z.number().optional().describe('Number of items per page (max 200)'), billable: z .number() .optional() .describe('Filter by billable status (0=non-billable, 1=billable)'), priority: z.number().optional().describe('Filter by priority level'), completed: z .number() .optional() .describe('Filter by completion status (0=incomplete, 1=complete)'), }) .partial(); // Generic get parameters const workflowGetParamsSchema = z .object({ id: z .union([z.string(), z.number()]) .describe( 'The entity ID (phase_id, milestone_id, project_task_id, or task_id for allocations)' ), }) .partial(); // Create/update data schema for workflow entities const workflowCreateUpdateDataSchema = z .object({ // Common fields name: z.string().optional().describe('Name/title of the entity'), notes: z.string().optional().describe('Notes or description'), start_date: z.string().optional().describe('Start date (YYYY-MM-DD)'), end_date: z.string().optional().describe('End date (YYYY-MM-DD)'), status: z.number().optional().describe('Status code'), active: z.number().optional().describe('Active status (0=archived, 1=active)'), // Phase-specific fields non_billable: z.number().optional().describe('Non-billable flag (0=billable, 1=non-billable)'), color: z.string().optional().describe('Color (hex code)'), default_hourly_rate: z.string().optional().describe('Default hourly rate'), budget_total: z.number().optional().describe('Total budget'), // Milestone-specific fields description: z.string().optional().describe('Milestone description'), date: z.string().optional().describe('Milestone date (YYYY-MM-DD)'), priority: z.number().optional().describe('Priority level (1-5)'), completed: z.number().optional().describe('Completion status (0=incomplete, 1=complete)'), completed_date: z.string().optional().describe('Completion date'), reminder_date: z.string().optional().describe('Reminder date'), reminder_sent: z.number().optional().describe('Reminder sent flag (0=not sent, 1=sent)'), created_by: z.number().optional().describe('User ID who created the milestone'), modified_by: z.number().optional().describe('User ID who last modified'), // Project task-specific fields task_names: z.string().optional().describe('Task name/title'), budget: z.number().optional().describe('Task budget'), budget_type: z.number().optional().describe('Budget type'), billable: z.number().optional().describe('Billable flag (0=non-billable, 1=billable)'), sort_order: z.number().optional().describe('Sort order'), dependencies: z.array(z.number()).optional().describe('Task dependencies'), estimated_hours: z.number().optional().describe('Estimated hours'), // Allocation-specific fields hours: z.number().optional().describe('Allocated hours'), task_type: z.number().optional().describe('Task type'), repeat_state: z.number().optional().describe('Repeat state'), repeat_end: z.string().optional().describe('Repeat end date'), // Bulk operations project_tasks: z .array( z.object({ task_names: z.string(), phase_id: z.number().optional(), notes: z.string().optional(), budget: z.number().optional(), budget_type: z.number().optional(), color: z.string().optional(), billable: z.number().optional(), priority: z.number().optional(), estimated_hours: z.number().optional(), }) ) .optional() .describe('Array of project tasks for bulk creation'), // Reorder operations task_order: z .array( z.object({ project_task_id: z.number(), sort_order: z.number(), }) ) .optional() .describe('Array of task IDs with new sort orders'), }) .partial(); // Main schema combining all parameters const manageProjectWorkflowSchema = withFormatParam( workflowBaseParamsSchema.extend({ ...workflowListParamsSchema.shape, ...workflowGetParamsSchema.shape, ...workflowCreateUpdateDataSchema.shape, }) ); export const manageProjectWorkflow = createTool( 'manage-project-workflow', 'Consolidated tool for managing project workflow entities (phases, milestones, project-tasks, allocations). Handles all project-specific operations including scheduling, task management, and resource allocation through a decision-tree approach.', manageProjectWorkflowSchema, async (params) => { const { format, entity_type, operation, id, ...restParams } = params; // Route based on entity type and operation switch (entity_type) { case 'phases': return handlePhaseOperations(operation, { id, ...restParams }, format); case 'milestones': return handleMilestoneOperations(operation, { id, ...restParams }, format); case 'project-tasks': return handleProjectTaskOperations(operation, { id, ...restParams }, format); case 'allocations': return handleAllocationOperations(operation, { id, ...restParams }, format); default: throw new Error(`Unsupported workflow entity type: ${entity_type}`); } } ); // Define proper parameter types based on our schemas type WorkflowParams = z.infer<typeof workflowListParamsSchema> & z.infer<typeof workflowGetParamsSchema> & z.infer<typeof workflowCreateUpdateDataSchema>; type WorkflowFormat = 'json' | 'xml'; // Phase operations handler async function handlePhaseOperations( operation: string, params: WorkflowParams, format: WorkflowFormat ): Promise<unknown> { const { id, project_id, start_date, end_date, ...otherParams } = params; const createUpdateParams = { ...otherParams, project_id, start_date, end_date }; switch (operation) { case 'list': return floatApi.getPaginated('/phases', otherParams, phasesResponseSchema, format); case 'get': return floatApi.get(`/phases/${id}`, phaseSchema, format); case 'create': return floatApi.post('/phases', createUpdateParams, phaseSchema, format); case 'update': return floatApi.patch(`/phases/${id}`, createUpdateParams, phaseSchema, format); case 'delete': await floatApi.delete(`/phases/${id}`, undefined, format); return { success: true, message: 'Phase deleted successfully' }; case 'list-phases-by-project': return floatApi.getPaginated('/phases', { project_id }, phasesResponseSchema, format); case 'get-phases-by-date-range': return floatApi.getPaginated( '/phases', { start_date, end_date }, phasesResponseSchema, format ); case 'get-active-phases': return floatApi.getPaginated('/phases', { active: 1 }, phasesResponseSchema, format); case 'get-phase-schedule': { const phases = await floatApi.getPaginated( '/phases', { project_id }, phasesResponseSchema, format ); // Sort by start date to create schedule view return phases.sort((a, b) => { const dateA = new Date(a.start_date || '').getTime(); const dateB = new Date(b.start_date || '').getTime(); return dateA - dateB; }); } default: throw new Error(`Unsupported phase operation: ${operation}`); } } // Milestone operations handler async function handleMilestoneOperations( operation: string, params: WorkflowParams, format: WorkflowFormat ): Promise<unknown> { const { id, project_id, ...otherParams } = params; const createUpdateParams = { ...otherParams, project_id }; switch (operation) { case 'list': return floatApi.getPaginated('/milestones', otherParams, milestonesResponseSchema, format); case 'get': return floatApi.get(`/milestones/${id}`, milestoneSchema, format); case 'create': return floatApi.post('/milestones', createUpdateParams, milestoneSchema, format); case 'update': return floatApi.patch(`/milestones/${id}`, createUpdateParams, milestoneSchema, format); case 'delete': await floatApi.delete(`/milestones/${id}`, undefined, format); return { success: true, message: 'Milestone deleted successfully' }; case 'get-project-milestones': return floatApi.getPaginated('/milestones', { project_id }, milestonesResponseSchema, format); case 'get-upcoming-milestones': { const today = new Date().toISOString().split('T')[0]; const upcomingMilestones = await floatApi.getPaginated( '/milestones', { date_from: today, completed: 0, }, milestonesResponseSchema, format ); return upcomingMilestones.sort((a, b) => { const dateA = new Date(a.date || a.start_date || '').getTime(); const dateB = new Date(b.date || b.start_date || '').getTime(); return dateA - dateB; }); } case 'get-overdue-milestones': { const currentDate = new Date().toISOString().split('T')[0]; const overdueMilestones = await floatApi.getPaginated( '/milestones', { date_to: currentDate, completed: 0, }, milestonesResponseSchema, format ); return overdueMilestones.filter((m) => { const milestoneDate = new Date(m.date || m.end_date || ''); return milestoneDate < new Date(); }); } case 'complete-milestone': { const completedDate = new Date().toISOString().split('T')[0]; return floatApi.patch( `/milestones/${id}`, { completed: 1, completed_date: completedDate, }, milestoneSchema, format ); } case 'get-milestone-reminders': return floatApi.getPaginated( '/milestones', { reminder_sent: 0, ...otherParams, }, milestonesResponseSchema, format ); default: throw new Error(`Unsupported milestone operation: ${operation}`); } } // Project task operations handler async function handleProjectTaskOperations( operation: string, params: WorkflowParams, format: WorkflowFormat ): Promise<unknown> { const { id, project_id, phase_id, project_tasks, task_order, ...otherParams } = params; const createUpdateParams = { ...otherParams, project_id, phase_id }; switch (operation) { case 'list': return floatApi.getPaginated( '/project_tasks', otherParams, projectTasksResponseSchema, format ); case 'get': return floatApi.get(`/project_tasks/${id}`, projectTaskSchema, format); case 'create': return floatApi.post('/project_tasks', createUpdateParams, projectTaskSchema, format); case 'update': return floatApi.patch(`/project_tasks/${id}`, createUpdateParams, projectTaskSchema, format); case 'delete': await floatApi.delete(`/project_tasks/${id}`, undefined, format); return { success: true, message: 'Project task deleted successfully' }; case 'get-project-tasks-by-project': return floatApi.getPaginated( '/project_tasks', { project_id }, projectTasksResponseSchema, format ); case 'get-project-tasks-by-phase': return floatApi.getPaginated( '/project_tasks', { phase_id }, projectTasksResponseSchema, format ); case 'bulk-create-project-tasks': { const results = []; const errors = []; const tasks = project_tasks || []; for (let index = 0; index < tasks.length; index++) { const taskData = { ...tasks[index], project_id }; try { const result = await floatApi.post('/project_tasks', taskData, projectTaskSchema, format); results.push({ index, success: true, data: result }); } catch (error) { errors.push({ index, success: false, error: error instanceof Error ? error.message : 'Unknown error', task: taskData, }); } } return { success: errors.length === 0, results, errors, summary: { total: tasks.length, successful: results.length, failed: errors.length, }, }; } case 'reorder-project-tasks': { const reorderResults = []; const reorderErrors = []; const orderUpdates = task_order || []; for (let index = 0; index < orderUpdates.length; index++) { const { project_task_id, sort_order } = orderUpdates[index]; try { const result = await floatApi.patch( `/project_tasks/${project_task_id}`, { sort_order }, projectTaskSchema, format ); reorderResults.push({ index, success: true, data: result }); } catch (error) { reorderErrors.push({ index, success: false, error: error instanceof Error ? error.message : 'Unknown error', task_id: project_task_id, }); } } return { success: reorderErrors.length === 0, results: reorderResults, errors: reorderErrors, summary: { total: orderUpdates.length, successful: reorderResults.length, failed: reorderErrors.length, }, }; } case 'archive-project-task': return floatApi.patch(`/project_tasks/${id}`, { active: 0 }, projectTaskSchema, format); case 'get-project-task-dependencies': { const task = await floatApi.get(`/project_tasks/${id}`, projectTaskSchema, format); const dependencies = task.dependencies || []; if (dependencies.length === 0) { return { dependencies: [], dependency_details: [] }; } // Fetch details of dependency tasks const dependencyDetails = []; for (const depId of dependencies) { try { const depTask = await floatApi.get(`/project_tasks/${depId}`, projectTaskSchema, format); dependencyDetails.push(depTask); } catch (error) { // Skip if dependency task not found continue; } } return { dependencies, dependency_details: dependencyDetails, }; } default: throw new Error(`Unsupported project task operation: ${operation}`); } } // Allocation operations handler async function handleAllocationOperations( operation: string, params: WorkflowParams, format: WorkflowFormat ): Promise<unknown> { const { id, ...otherParams } = params; switch (operation) { case 'list': return floatApi.getPaginated('/tasks', otherParams, allocationsResponseSchema, format); case 'get': return floatApi.get(`/tasks/${id}`, allocationSchema, format); case 'create': return floatApi.post('/tasks', otherParams, allocationSchema, format); case 'update': return floatApi.patch(`/tasks/${id}`, otherParams, allocationSchema, format); case 'delete': await floatApi.delete(`/tasks/${id}`, undefined, format); return { success: true, message: 'Allocation deleted successfully' }; default: throw new Error(`Unsupported allocation operation: ${operation}`); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/asachs01/float-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server