Skip to main content
Glama
time-tracking-tools.ts15.6 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { createClickUpClient } from '../clickup-client/index.js'; import { createEnhancedTimeTrackingClient } from '../clickup-client/time-tracking-enhanced.js'; import { // TeamIdSchema, // TimerIdSchema, // CreateTimeEntrySchema, // UpdateTimeEntrySchema, // GetTimeEntriesSchema, // TimeEntryTagSchema } from '../schemas/time-tracking-schemas.js'; // Create clients const clickUpClient = createClickUpClient(); const timeTrackingClient = createEnhancedTimeTrackingClient(clickUpClient); export function setupTimeTrackingTools(server: McpServer): void { // ======================================== // TIME ENTRY MANAGEMENT OPERATIONS // ======================================== server.tool( 'clickup_get_time_entries', 'Get time entries for a team with filtering options. Supports filtering by date range, user, task, and project.', { team_id: z.string().min(1).describe('The ID of the team to get time entries for'), start_date: z.number().positive().optional().describe('Filter by start date (Unix timestamp)'), end_date: z.number().positive().optional().describe('Filter by end date (Unix timestamp)'), assignee: z.number().positive().optional().describe('Filter by user ID'), include_task_tags: z.boolean().optional().default(false).describe('Include task tags in response'), include_location_names: z.boolean().optional().default(false).describe('Include location names'), space_id: z.string().optional().describe('Filter by space ID'), folder_id: z.string().optional().describe('Filter by folder ID'), list_id: z.string().optional().describe('Filter by list ID'), task_id: z.string().optional().describe('Filter by task ID') }, async ({ team_id, start_date, end_date, assignee, include_task_tags, include_location_names, space_id, folder_id, list_id, task_id }) => { try { const params = { start_date, end_date, assignee, include_task_tags, include_location_names, space_id, folder_id, list_id, task_id }; const timeEntries = await timeTrackingClient.getTimeEntries(team_id, params); return { content: [{ type: 'text', text: `Time entries for team ${team_id}:\n\n${JSON.stringify(timeEntries, null, 2)}` }] }; } catch (error: any) { console.error('Error getting time entries:', error); return { content: [{ type: 'text', text: `Error getting time entries: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_create_time_entry', 'Create a new time entry for time tracking. Can be used for manual time logging or creating timer-based entries.', { team_id: z.string().min(1).describe('The ID of the team to create the time entry for'), description: z.string().min(1).describe('Description of the time entry'), start: z.number().positive().describe('Start time (Unix timestamp in milliseconds)'), billable: z.boolean().default(false).describe('Whether the time is billable'), end: z.number().positive().optional().describe('End time (Unix timestamp in milliseconds)'), task_id: z.string().optional().describe('Associated task ID'), assignee: z.number().positive().optional().describe('User ID for the time entry'), tags: z.array(z.object({ name: z.string().min(1).describe('Tag name'), tag_fg: z.string().optional().describe('Tag foreground color'), tag_bg: z.string().optional().describe('Tag background color') })).optional().describe('Array of tags for the time entry') }, async ({ team_id, description, start, billable, end, task_id, assignee, tags }) => { try { const params = { description, start, billable, end, task_id, assignee, tags }; const timeEntry = await timeTrackingClient.createTimeEntry(team_id, params); return { content: [{ type: 'text', text: `Time entry created successfully!\n\n${JSON.stringify(timeEntry, null, 2)}` }] }; } catch (error: any) { console.error('Error creating time entry:', error); return { content: [{ type: 'text', text: `Error creating time entry: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_update_time_entry', 'Update an existing time entry. Can modify description, times, billable status, and associated task.', { team_id: z.string().min(1).describe('The ID of the team'), timer_id: z.string().min(1).describe('The ID of the time entry to update'), description: z.string().min(1).optional().describe('New description for the time entry'), start: z.number().positive().optional().describe('New start time (Unix timestamp in milliseconds)'), end: z.number().positive().optional().describe('New end time (Unix timestamp in milliseconds)'), billable: z.boolean().optional().describe('Update billable status'), task_id: z.string().optional().describe('Change associated task ID'), tags: z.array(z.object({ name: z.string().min(1).describe('Tag name'), tag_fg: z.string().optional().describe('Tag foreground color'), tag_bg: z.string().optional().describe('Tag background color') })).optional().describe('Update tags for the time entry') }, async ({ team_id, timer_id, description, start, end, billable, task_id, tags }) => { try { const params = { description, start, end, billable, task_id, tags }; const updatedTimeEntry = await timeTrackingClient.updateTimeEntry(team_id, timer_id, params); return { content: [{ type: 'text', text: `Time entry updated successfully!\n\n${JSON.stringify(updatedTimeEntry, null, 2)}` }] }; } catch (error: any) { console.error('Error updating time entry:', error); return { content: [{ type: 'text', text: `Error updating time entry: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_delete_time_entry', 'Delete a time entry from ClickUp. This action cannot be undone.', { team_id: z.string().min(1).describe('The ID of the team'), timer_id: z.string().min(1).describe('The ID of the time entry to delete') }, async ({ team_id, timer_id }) => { try { await timeTrackingClient.deleteTimeEntry(team_id, timer_id); return { content: [{ type: 'text', text: `Time entry ${timer_id} deleted successfully from team ${team_id}.` }] }; } catch (error: any) { console.error('Error deleting time entry:', error); return { content: [{ type: 'text', text: `Error deleting time entry: ${error.message}` }], isError: true }; } } ); // ======================================== // TIMER OPERATIONS // ======================================== server.tool( 'clickup_get_running_timers', 'Get currently running timers for a team. Shows active time tracking sessions.', { team_id: z.string().min(1).describe('The ID of the team to get running timers for'), assignee: z.number().positive().optional().describe('Filter by specific user ID') }, async ({ team_id, assignee }) => { try { const runningTimers = await timeTrackingClient.getRunningTimers(team_id, assignee); return { content: [{ type: 'text', text: `Running timers for team ${team_id}:\n\n${JSON.stringify(runningTimers, null, 2)}` }] }; } catch (error: any) { console.error('Error getting running timers:', error); return { content: [{ type: 'text', text: `Error getting running timers: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_start_timer', 'Start a timer for time tracking. Creates an active time tracking session.', { team_id: z.string().min(1).describe('The ID of the team'), timer_id: z.string().min(1).describe('The ID of the time entry to start timing'), start: z.number().positive().optional().describe('Custom start time (Unix timestamp in milliseconds, defaults to current time)') }, async ({ team_id, timer_id, start }) => { try { await timeTrackingClient.startTimer(team_id, timer_id, start); return { content: [{ type: 'text', text: `Timer started successfully for time entry ${timer_id} in team ${team_id}.` }] }; } catch (error: any) { console.error('Error starting timer:', error); return { content: [{ type: 'text', text: `Error starting timer: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_stop_timer', 'Stop a running timer. Ends the active time tracking session and records the duration.', { team_id: z.string().min(1).describe('The ID of the team'), timer_id: z.string().min(1).describe('The ID of the time entry to stop timing'), end: z.number().positive().optional().describe('Custom end time (Unix timestamp in milliseconds, defaults to current time)') }, async ({ team_id, timer_id, end }) => { try { await timeTrackingClient.stopTimer(team_id, timer_id, end); return { content: [{ type: 'text', text: `Timer stopped successfully for time entry ${timer_id} in team ${team_id}.` }] }; } catch (error: any) { console.error('Error stopping timer:', error); return { content: [{ type: 'text', text: `Error stopping timer: ${error.message}` }], isError: true }; } } ); // ======================================== // TIME ANALYTICS & REPORTING // ======================================== server.tool( 'clickup_get_time_summary', 'Get time tracking summary and analytics. Provides aggregated time data with breakdowns by user and task.', { team_id: z.string().min(1).describe('The ID of the team to get time summary for'), start_date: z.number().positive().optional().describe('Filter by start date (Unix timestamp)'), end_date: z.number().positive().optional().describe('Filter by end date (Unix timestamp)'), assignee: z.number().positive().optional().describe('Filter by user ID'), task_id: z.string().optional().describe('Filter by task ID'), list_id: z.string().optional().describe('Filter by list ID'), folder_id: z.string().optional().describe('Filter by folder ID'), space_id: z.string().optional().describe('Filter by space ID') }, async ({ team_id, start_date, end_date, assignee, task_id, list_id, folder_id, space_id }) => { try { const params = { start_date, end_date, assignee, task_id, list_id, folder_id, space_id }; const timeSummary = await timeTrackingClient.getTimeSummary(team_id, params); // Format durations for better readability const formatDuration = (ms: number) => timeTrackingClient.formatDuration(ms); const formattedSummary = { ...timeSummary, total_duration_formatted: formatDuration(timeSummary.total_duration), billable_duration_formatted: formatDuration(timeSummary.billable_duration), non_billable_duration_formatted: formatDuration(timeSummary.non_billable_duration) }; return { content: [{ type: 'text', text: `Time summary for team ${team_id}:\n\n${JSON.stringify(formattedSummary, null, 2)}` }] }; } catch (error: any) { console.error('Error getting time summary:', error); return { content: [{ type: 'text', text: `Error getting time summary: ${error.message}` }], isError: true }; } } ); // ======================================== // HELPER TOOLS // ======================================== server.tool( 'clickup_create_timer_entry', 'Create a new time entry and immediately start the timer. Convenient for starting time tracking in one step.', { team_id: z.string().min(1).describe('The ID of the team'), description: z.string().min(1).describe('Description of what you are working on'), task_id: z.string().optional().describe('Associated task ID'), billable: z.boolean().default(false).describe('Whether the time is billable'), tags: z.array(z.object({ name: z.string().min(1).describe('Tag name') })).optional().describe('Array of tags for the time entry') }, async ({ team_id, description, task_id, billable, tags }) => { try { const currentTime = timeTrackingClient.getCurrentTimestamp(); // Create time entry const timeEntry = await timeTrackingClient.createTimeEntry(team_id, { description, start: currentTime, billable, task_id, tags }); // Start the timer await timeTrackingClient.startTimer(team_id, timeEntry.id); return { content: [{ type: 'text', text: `Timer started successfully! Time entry created and timer is now running.\n\nTime Entry: ${JSON.stringify(timeEntry, null, 2)}` }] }; } catch (error: any) { console.error('Error creating timer entry:', error); return { content: [{ type: 'text', text: `Error creating timer entry: ${error.message}` }], isError: true }; } } ); server.tool( 'clickup_format_duration', 'Format a duration from milliseconds to human-readable format. Useful for displaying time tracking data.', { milliseconds: z.number().min(0).describe('Duration in milliseconds'), include_seconds: z.boolean().optional().default(true).describe('Whether to include seconds in the formatted output'), format: z.enum(['milliseconds', 'seconds', 'minutes', 'hours']).optional().default('milliseconds').describe('Convert to specific time unit') }, async ({ milliseconds, include_seconds, format }) => { try { const formattedDuration = timeTrackingClient.formatDuration(milliseconds, include_seconds); const convertedValue = timeTrackingClient.convertDuration(milliseconds, format); return { content: [{ type: 'text', text: `Duration formatting:\n\nOriginal: ${milliseconds} milliseconds\nFormatted: ${formattedDuration}\nConverted to ${format}: ${convertedValue}` }] }; } catch (error: any) { console.error('Error formatting duration:', error); return { content: [{ type: 'text', text: `Error formatting duration: ${error.message}` }], isError: true }; } } ); }

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/Chykalophia/ClickUp-MCP-Server---Enhanced'

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