Skip to main content
Glama
index.ts46.8 kB
import { z } from 'zod'; import { ClockifyApiClient, UserService, WorkspaceService, ProjectService, ClientService, TimeEntryService, TagService, TaskService, ReportService, CustomFieldService, } from '../api/services/index.js'; import * as schemas from './schemas.js'; import { ConfigurationManager } from '../config/index.js'; import { RestrictionMiddleware } from '../middleware/restrictions.js'; interface ToolDefinition { name: string; description: string; category: string; inputSchema: z.ZodSchema; handler: (args: any) => Promise<any>; priority?: number; // Lower number = higher priority when filtering } export class ClockifyTools { private userService: UserService; private workspaceService: WorkspaceService; private projectService: ProjectService; private clientService: ClientService; private timeEntryService: TimeEntryService; private tagService: TagService; private taskService: TaskService; private reportService: ReportService; private customFieldService: CustomFieldService; private restrictionMiddleware: RestrictionMiddleware; private config: ConfigurationManager; constructor(apiKey: string, config: ConfigurationManager) { const client = new ClockifyApiClient(apiKey, config.getApiUrl()); this.userService = new UserService(client); this.workspaceService = new WorkspaceService(client); this.projectService = new ProjectService(client); this.clientService = new ClientService(client); this.timeEntryService = new TimeEntryService(client); this.tagService = new TagService(client); this.taskService = new TaskService(client); this.reportService = new ReportService(client); this.customFieldService = new CustomFieldService(client); this.restrictionMiddleware = new RestrictionMiddleware(config); this.config = config; } private isProtectedProject(projectId: string): { isProtected: boolean; reason?: string } { const defaultProjectId = this.config.getDefaultProjectId(); const restrictions = this.config.getRestrictions(); // Check if it's the default project if (defaultProjectId && projectId === defaultProjectId) { return { isProtected: true, reason: 'This is the configured default project. Please update your configuration to remove the default project setting first.', }; } // Check if it's the only allowed project in a restricted setup if ( restrictions.allowedProjects && restrictions.allowedProjects.length === 1 && restrictions.allowedProjects[0] === projectId ) { return { isProtected: true, reason: 'This is the only allowed project in the configuration. Please update your allowedProjects configuration first.', }; } return { isProtected: false }; } private isConfiguredProject(projectId: string): boolean { const defaultProjectId = this.config.getDefaultProjectId(); const restrictions = this.config.getRestrictions(); return ( (defaultProjectId !== undefined && projectId === defaultProjectId) || (restrictions.allowedProjects !== undefined && restrictions.allowedProjects.includes(projectId)) ); } private getAllTools(): ToolDefinition[] { return [ // User Tools { name: 'get_current_user', description: 'Get information about the currently authenticated user', category: 'user', priority: 1, inputSchema: z.object({}), handler: async () => { const user = await this.userService.getCurrentUser(); return { success: true, data: user }; }, }, { name: 'get_user', description: 'Get information about a specific user', category: 'user', priority: 3, inputSchema: schemas.workspaceIdSchema.extend({ userId: schemas.objectIdSchema.describe('The user ID'), }), handler: async ( input: z.infer<typeof schemas.workspaceIdSchema & typeof schemas.userIdSchema> ) => { const user = await this.userService.getUserById(input.workspaceId, input.userId); return { success: true, data: user }; }, }, { name: 'list_users', description: 'List all users in a workspace', category: 'user', priority: 2, inputSchema: schemas.workspaceIdSchema.extend(schemas.searchUsersSchema.shape), handler: async (input: any) => { const users = await this.userService.getAllUsers(input.workspaceId, input); return { success: true, data: users }; }, }, { name: 'find_user_by_name', description: 'Find users by name (partial match)', category: 'search', priority: 5, inputSchema: schemas.workspaceIdSchema.extend({ name: z.string().describe('Name to search for (partial match)'), }), handler: async (input: any) => { const users = await this.userService.findUserByName(input.workspaceId, input.name); return { success: true, data: users }; }, }, // Workspace Tools { name: 'list_workspaces', description: 'List all workspaces accessible to the user (filtered by restrictions)', category: 'workspace', priority: 1, inputSchema: z.object({}), handler: async () => { const workspaces = await this.workspaceService.getAllWorkspaces(); const filteredWorkspaces = this.restrictionMiddleware.filterWorkspaces(workspaces); return { success: true, data: filteredWorkspaces }; }, }, { name: 'get_workspace', category: 'workspace', priority: 2, description: 'Get details of a specific workspace', inputSchema: schemas.workspaceIdSchema, handler: async (input: z.infer<typeof schemas.workspaceIdSchema>) => { const workspace = await this.workspaceService.getWorkspaceById(input.workspaceId); return { success: true, data: workspace }; }, }, { name: 'update_workspace', category: 'workspace', priority: 3, description: 'Update workspace settings', inputSchema: schemas.workspaceIdSchema.extend({ name: z.string().optional().describe('New workspace name'), imageUrl: z.string().optional().describe('New workspace image URL'), settings: z .object({ timeRoundingInReports: z .boolean() .optional() .describe('Enable time rounding in reports'), onlyAdminsSeeBillableRates: z .boolean() .optional() .describe('Only admins can see billable rates'), onlyAdminsCreateProject: z .boolean() .optional() .describe('Only admins can create projects'), onlyAdminsSeeDashboard: z .boolean() .optional() .describe('Only admins can see dashboard'), defaultBillableProjects: z .boolean() .optional() .describe('Make projects billable by default'), lockTimeEntries: z.string().optional().describe('Time entry locking policy'), round: z .object({ round: z.string().describe('Rounding rule'), minutes: z.string().describe('Rounding minutes'), }) .optional() .describe('Time rounding settings'), }) .optional() .describe('Workspace settings to update'), }), handler: async (input: any) => { // Strong protection: workspace changes can affect all configured projects const restrictions = this.config.getRestrictions(); const defaultProjectId = this.config.getDefaultProjectId(); if (defaultProjectId || restrictions.allowedProjects) { // Specific dangerous settings changes if (input.settings?.onlyAdminsCreateProject === true) { throw new Error( 'Cannot enable "only admins create project" setting when projects are configured. This could prevent access to project functionality.' ); } if (input.settings?.lockTimeEntries) { console.warn( 'Warning: Changing time entry locking policy with configured projects. This may affect existing time tracking workflows.' ); } // General warning for any settings change console.warn( 'Warning: Updating workspace settings with configured projects. Ensure these changes do not affect required project functionality.' ); } const { workspaceId, ...data } = input; const workspace = await this.workspaceService.updateWorkspace(workspaceId, data); return { success: true, data: workspace }; }, }, { name: 'delete_workspace', category: 'workspace', priority: 4, description: 'Delete a workspace', inputSchema: schemas.workspaceIdSchema, handler: async (input: z.infer<typeof schemas.workspaceIdSchema>) => { // Absolute protection: never allow deleting workspace with configured projects const restrictions = this.config.getRestrictions(); const defaultProjectId = this.config.getDefaultProjectId(); const defaultWorkspaceId = this.config.getDefaultWorkspaceId(); // Check if this is the default workspace if (defaultWorkspaceId && input.workspaceId === defaultWorkspaceId) { throw new Error( 'Cannot delete the configured default workspace. Please update your configuration first.' ); } // Check if this workspace contains configured projects if (defaultProjectId || restrictions.allowedProjects) { throw new Error( 'Cannot delete workspace: This workspace contains configured projects. Please update your configuration to remove project restrictions first.' ); } await this.workspaceService.deleteWorkspace(input.workspaceId); return { success: true, message: 'Workspace deleted successfully' }; }, }, // Project Tools { name: 'list_projects', category: 'project', priority: 1, description: 'List all projects in a workspace (filtered by restrictions)', inputSchema: schemas.searchProjectsSchema, handler: async (input: z.infer<typeof schemas.searchProjectsSchema>) => { const projects = await this.projectService.getAllProjects(input.workspaceId, input); const filteredProjects = this.restrictionMiddleware.filterProjects(projects); return { success: true, data: filteredProjects }; }, }, { name: 'get_project', category: 'project', priority: 2, description: 'Get details of a specific project', inputSchema: schemas.workspaceIdSchema.extend({ projectId: schemas.objectIdSchema.describe('The project ID'), }), handler: async (input: any) => { const project = await this.projectService.getProjectById( input.workspaceId, input.projectId ); return { success: true, data: project }; }, }, { name: 'create_project', category: 'project', priority: 3, description: 'Create a new project', inputSchema: schemas.createProjectSchema, handler: async (input: z.infer<typeof schemas.createProjectSchema>) => { const project = await this.projectService.createProject(input.workspaceId, input); return { success: true, data: project }; }, }, { name: 'update_project', category: 'project', priority: 4, description: 'Update an existing project', inputSchema: schemas.updateProjectSchema, handler: async (input: z.infer<typeof schemas.updateProjectSchema>) => { const { workspaceId, projectId, ...data } = input; // Check if this is a configured project and if critical fields are being changed if (this.isConfiguredProject(projectId)) { // Don't allow archiving configured projects through update if (data.archived === true) { throw new Error( 'Cannot archive a configured project through update. Please update your configuration first.' ); } // Warn about critical field changes but allow them (name, client changes might be intentional) if (data.name || data.clientId !== undefined) { console.warn( `Warning: Updating critical fields of configured project ${projectId}. Ensure this change is intentional.` ); } } const project = await this.projectService.updateProject(workspaceId, projectId, data); return { success: true, data: project }; }, }, { name: 'archive_project', category: 'project', priority: 5, description: 'Archive a project', inputSchema: schemas.workspaceIdSchema.extend({ projectId: schemas.objectIdSchema.describe('The project ID to archive'), }), handler: async (input: any) => { const protection = this.isProtectedProject(input.projectId); if (protection.isProtected) { throw new Error(`Cannot archive project: ${protection.reason}`); } const project = await this.projectService.archiveProject( input.workspaceId, input.projectId ); return { success: true, data: project }; }, }, { name: 'delete_project', category: 'project', priority: 6, description: 'Delete a project', inputSchema: schemas.workspaceIdSchema.extend({ projectId: schemas.objectIdSchema.describe('The project ID to delete'), }), handler: async (input: any) => { const protection = this.isProtectedProject(input.projectId); if (protection.isProtected) { throw new Error(`Cannot delete project: ${protection.reason}`); } await this.projectService.deleteProject(input.workspaceId, input.projectId); return { success: true, message: 'Project deleted successfully' }; }, }, { name: 'add_user_to_project', category: 'project', priority: 7, description: 'Add a user to a project', inputSchema: schemas.workspaceIdSchema.extend({ projectId: schemas.objectIdSchema.describe('The project ID'), userId: schemas.objectIdSchema.describe('The user ID to add'), hourlyRate: z .object({ amount: z.number().describe('Hourly rate amount'), currency: z.string().describe('Currency code (e.g., USD, EUR)'), }) .optional() .describe('Hourly rate for this user on this project'), }), handler: async (input: any) => { // No restriction needed for adding users to projects const result = await this.projectService.addUserToProject( input.workspaceId, input.projectId, input.userId, input.hourlyRate ? { hourlyRate: input.hourlyRate } : undefined ); return { success: true, data: result }; }, }, { name: 'remove_user_from_project', category: 'project', priority: 8, description: 'Remove a user from a project', inputSchema: schemas.workspaceIdSchema.extend({ projectId: schemas.objectIdSchema.describe('The project ID'), userId: schemas.objectIdSchema.describe('The user ID to remove'), }), handler: async (input: any) => { // Check if this is a configured project - don't allow removing access if (this.isConfiguredProject(input.projectId)) { throw new Error( 'Cannot remove users from a configured project. This could break access to required project functionality.' ); } await this.projectService.removeUserFromProject( input.workspaceId, input.projectId, input.userId ); return { success: true, message: 'User removed from project successfully' }; }, }, { name: 'find_project_by_name', category: 'search', priority: 2, description: 'Find projects by name', inputSchema: schemas.workspaceIdSchema.extend({ name: z.string().describe('Project name to search for'), }), handler: async (input: any) => { const projects = await this.projectService.findProjectByName( input.workspaceId, input.name ); return { success: true, data: projects }; }, }, // Client Tools { name: 'list_clients', category: 'client', priority: 1, description: 'List all clients in a workspace', inputSchema: schemas.workspaceIdSchema.extend({ archived: z.boolean().optional().describe('Include archived clients'), }), handler: async (input: any) => { const clients = await this.clientService.getAllClients(input.workspaceId, input); return { success: true, data: clients }; }, }, { name: 'get_client', category: 'client', priority: 2, description: 'Get details of a specific client', inputSchema: schemas.workspaceIdSchema.extend({ clientId: schemas.objectIdSchema.describe('The client ID'), }), handler: async (input: any) => { const client = await this.clientService.getClientById(input.workspaceId, input.clientId); return { success: true, data: client }; }, }, { name: 'create_client', category: 'client', priority: 3, description: 'Create a new client', inputSchema: schemas.createClientSchema, handler: async (input: z.infer<typeof schemas.createClientSchema>) => { const { workspaceId, ...data } = input; const client = await this.clientService.createClient(workspaceId, data); return { success: true, data: client }; }, }, { name: 'update_client', category: 'client', priority: 4, description: 'Update an existing client', inputSchema: schemas.workspaceIdSchema.extend({ clientId: schemas.objectIdSchema.describe('The client ID'), name: z.string().optional(), email: z.string().email().optional(), address: z.string().optional(), note: z.string().optional(), archived: z.boolean().optional(), }), handler: async (input: any) => { const { workspaceId, clientId, ...data } = input; const client = await this.clientService.updateClient(workspaceId, clientId, data); return { success: true, data: client }; }, }, { name: 'delete_client', category: 'client', priority: 5, description: 'Delete a client', inputSchema: schemas.workspaceIdSchema.extend({ clientId: schemas.objectIdSchema.describe('The client ID to delete'), }), handler: async (input: any) => { // Check if any configured projects depend on this client const restrictions = this.config.getRestrictions(); const defaultProjectId = this.config.getDefaultProjectId(); // If we have configured projects, check if they use this client if (defaultProjectId || restrictions.allowedProjects) { // Get projects that might be using this client try { const projects = await this.projectService.getProjectsForClient( input.workspaceId, input.clientId ); const configuredProjects = projects.filter(p => this.isConfiguredProject(p.id)); if (configuredProjects.length > 0) { const projectNames = configuredProjects.map(p => p.name).join(', '); throw new Error( `Cannot delete client: The following configured projects depend on this client: ${projectNames}. Please update your project configuration first.` ); } } catch (error) { // If we can't check, err on the side of caution if (error instanceof Error && !error.message.includes('Cannot delete client')) { console.warn( 'Warning: Unable to verify client dependencies for configured projects. Proceeding with deletion.' ); } else { throw error; } } } await this.clientService.deleteClient(input.workspaceId, input.clientId); return { success: true, message: 'Client deleted successfully' }; }, }, // Time Entry Tools { name: 'create_time_entry', category: 'timeEntry', priority: 1, description: 'Create a new time entry (start tracking time)', inputSchema: schemas.createTimeEntrySchema, handler: async (input: z.infer<typeof schemas.createTimeEntrySchema>) => { const { workspaceId, ...data } = input; const entry = await this.timeEntryService.createTimeEntry(workspaceId, data); return { success: true, data: entry }; }, }, { name: 'update_time_entry', category: 'timeEntry', priority: 3, description: 'Update an existing time entry', inputSchema: schemas.updateTimeEntrySchema, handler: async (input: z.infer<typeof schemas.updateTimeEntrySchema>) => { const { workspaceId, timeEntryId, ...data } = input; // If no projectId provided, preserve the existing one to avoid workspace validation errors if (!data.projectId) { try { const existing = await this.timeEntryService.getTimeEntryById( workspaceId, timeEntryId ); if (existing.projectId) { data.projectId = existing.projectId; } } catch (error) { // If we can't fetch existing entry, proceed without project preservation } } const entry = await this.timeEntryService.updateTimeEntry(workspaceId, timeEntryId, data); return { success: true, data: entry }; }, }, { name: 'delete_time_entry', category: 'timeEntry', priority: 4, description: 'Delete a time entry', inputSchema: schemas.workspaceIdSchema.extend({ timeEntryId: schemas.objectIdSchema.describe('The time entry ID to delete'), }), handler: async (input: any) => { await this.timeEntryService.deleteTimeEntry(input.workspaceId, input.timeEntryId); return { success: true, message: 'Time entry deleted successfully' }; }, }, { name: 'get_time_entries', category: 'timeEntry', priority: 2, description: 'Get time entries for a user', inputSchema: schemas.workspaceIdSchema.extend({ userId: z.string().describe('The user ID'), start: z.string().optional().describe('Start date in ISO format'), end: z.string().optional().describe('End date in ISO format'), projectId: z.string().optional().describe('Filter by project ID'), description: z.string().optional().describe('Filter by description'), }), handler: async (input: any) => { const { workspaceId, userId, projectId, ...options } = input; const entries = await this.timeEntryService.getTimeEntriesForUser( workspaceId, userId, projectId ? { ...options, project: projectId } : options ); return { success: true, data: entries }; }, }, { name: 'get_running_timer', category: 'timeEntry', priority: 5, description: 'Get the currently running timer for a user', inputSchema: schemas.workspaceIdSchema.extend({ userId: schemas.objectIdSchema.describe('The user ID'), }), handler: async (input: any) => { const entry = await this.timeEntryService.getRunningTimeEntry( input.workspaceId, input.userId ); return { success: true, data: entry }; }, }, { name: 'get_time_entry_by_id', category: 'timeEntry', priority: 2, description: 'Get a specific time entry by ID', inputSchema: schemas.workspaceIdSchema.extend({ timeEntryId: schemas.objectIdSchema.describe('The time entry ID'), hydrated: z .boolean() .optional() .describe('Include related entities (project, task, user, etc.)'), }), handler: async (input: any) => { const entry = await this.timeEntryService.getTimeEntryById( input.workspaceId, input.timeEntryId ); return { success: true, data: entry }; }, }, { name: 'duplicate_time_entry', category: 'timeEntry', priority: 7, description: 'Duplicate an existing time entry', inputSchema: schemas.workspaceIdSchema.extend({ timeEntryId: schemas.objectIdSchema.describe('The time entry ID to duplicate'), }), handler: async (input: any) => { const entry = await this.timeEntryService.duplicateTimeEntry( input.workspaceId, input.timeEntryId ); return { success: true, data: entry }; }, }, { name: 'start_timer', category: 'timeEntry', priority: 5, description: 'Start a new timer (creates running time entry)', inputSchema: schemas.startTimerSchema, handler: async (input: z.infer<typeof schemas.startTimerSchema>) => { const timerData = { ...input, start: new Date().toISOString(), // Don't include end time to create a running timer }; const entry = await this.timeEntryService.createTimeEntry(input.workspaceId, timerData); return { success: true, data: entry }; }, }, { name: 'stop_timer', category: 'timeEntry', priority: 6, description: 'Stop the currently running timer', inputSchema: schemas.stopTimerSchema, handler: async (input: z.infer<typeof schemas.stopTimerSchema>) => { const userId = input.userId || (await this.userService.getCurrentUser()).id; const entry = await this.timeEntryService.stopRunningTimer(input.workspaceId, userId, { end: new Date().toISOString(), }); return { success: true, data: entry }; }, }, { name: 'get_today_entries', category: 'timeEntry', priority: 7, description: 'Get all time entries for today', inputSchema: schemas.workspaceIdSchema.extend({ userId: schemas.objectIdSchema.describe('The user ID'), }), handler: async (input: any) => { const entries = await this.timeEntryService.getTodayTimeEntries( input.workspaceId, input.userId ); return { success: true, data: entries }; }, }, { name: 'get_week_entries', category: 'timeEntry', priority: 8, description: 'Get all time entries for the current week', inputSchema: schemas.workspaceIdSchema.extend({ userId: schemas.objectIdSchema.describe('The user ID'), }), handler: async (input: any) => { const entries = await this.timeEntryService.getWeekTimeEntries( input.workspaceId, input.userId ); return { success: true, data: entries }; }, }, { name: 'get_month_entries', category: 'timeEntry', priority: 9, description: 'Get all time entries for a specific month', inputSchema: schemas.workspaceIdSchema.extend({ userId: z.string().describe('The user ID'), year: z.number().optional().describe('Year (defaults to current year)'), month: z.number().optional().describe('Month (0-11, defaults to current month)'), }), handler: async (input: any) => { const entries = await this.timeEntryService.getMonthTimeEntries( input.workspaceId, input.userId, input.year, input.month ); return { success: true, data: entries }; }, }, { name: 'bulk_edit_time_entries', category: 'bulk', priority: 1, description: 'Bulk edit multiple time entries', inputSchema: schemas.bulkTimeEntriesSchema, handler: async (input: z.infer<typeof schemas.bulkTimeEntriesSchema>) => { const { workspaceId, timeEntryIds, action, updates } = input; // For bulk updates that change project assignment, validate the change if (action === 'UPDATE' && updates?.projectId) { // Check if we're trying to move entries away from a configured project const restrictions = this.config.getRestrictions(); const defaultProjectId = this.config.getDefaultProjectId(); // If we have configured projects, warn about bulk moves if (defaultProjectId || restrictions.allowedProjects) { console.warn( 'Warning: Bulk operation is changing project assignments. Ensure this does not affect configured project requirements.' ); } // Don't allow moving TO a configured project in bulk (could overwhelm it) if (this.isConfiguredProject(updates.projectId)) { throw new Error( 'Cannot bulk assign time entries to a configured project. Please assign individually to avoid overwhelming the configured project.' ); } } if (action === 'DELETE') { await this.timeEntryService.bulkDeleteTimeEntries(workspaceId, timeEntryIds); return { success: true, message: 'Time entries deleted successfully' }; } else { const result = await this.timeEntryService.bulkEditTimeEntries( workspaceId, timeEntryIds, updates || {} ); return { success: true, data: result }; } }, }, // Tag Tools { name: 'list_tags', category: 'tag', priority: 1, description: 'List all tags in a workspace', inputSchema: schemas.workspaceIdSchema.extend({ archived: z.boolean().optional().describe('Include archived tags'), }), handler: async (input: any) => { const tags = await this.tagService.getAllTags(input.workspaceId, input); return { success: true, data: tags }; }, }, { name: 'create_tag', category: 'tag', priority: 2, description: 'Create a new tag', inputSchema: schemas.createTagSchema, handler: async (input: z.infer<typeof schemas.createTagSchema>) => { const { workspaceId, ...data } = input; const tag = await this.tagService.createTag(workspaceId, data); return { success: true, data: tag }; }, }, { name: 'create_multiple_tags', category: 'tag', priority: 3, description: 'Create multiple tags at once', inputSchema: schemas.workspaceIdSchema.extend({ names: z.array(z.string()).describe('Array of tag names to create'), }), handler: async (input: any) => { const tags = await this.tagService.createMultipleTags(input.workspaceId, input.names); return { success: true, data: tags }; }, }, { name: 'delete_tag', category: 'tag', priority: 4, description: 'Delete a tag', inputSchema: schemas.workspaceIdSchema.extend({ tagId: schemas.objectIdSchema.describe('The tag ID to delete'), }), handler: async (input: any) => { // Tags can be deleted freely as they don't fundamentally affect configured projects // But warn if workspace has configured projects that might use this tag const restrictions = this.config.getRestrictions(); const defaultProjectId = this.config.getDefaultProjectId(); if (defaultProjectId || restrictions.allowedProjects) { console.warn( 'Warning: Deleting tag from workspace with configured projects. Ensure this tag is not required for project workflows.' ); } await this.tagService.deleteTag(input.workspaceId, input.tagId); return { success: true, message: 'Tag deleted successfully' }; }, }, // Task Tools { name: 'list_tasks', category: 'task', priority: 1, description: 'List all tasks in a project', inputSchema: schemas.workspaceIdSchema.extend({ projectId: schemas.objectIdSchema.describe('The project ID'), isActive: z.boolean().optional().describe('Filter by active status'), }), handler: async (input: any) => { const tasks = await this.taskService.getAllTasks( input.workspaceId, input.projectId, input ); return { success: true, data: tasks }; }, }, { name: 'create_task', category: 'task', priority: 2, description: 'Create a new task in a project', inputSchema: schemas.createTaskSchema, handler: async (input: z.infer<typeof schemas.createTaskSchema>) => { const { workspaceId, projectId, ...data } = input; const task = await this.taskService.createTask(workspaceId, projectId, data); return { success: true, data: task }; }, }, { name: 'update_task', category: 'task', priority: 3, description: 'Update an existing task', inputSchema: schemas.workspaceIdSchema.extend({ projectId: schemas.objectIdSchema.describe('The project ID'), taskId: schemas.objectIdSchema.describe('The task ID'), name: z.string().optional(), assigneeIds: z.array(z.string()).optional(), estimate: z.string().optional(), status: z.enum(['ACTIVE', 'DONE']).optional(), billable: z.boolean().optional(), }), handler: async (input: any) => { const { workspaceId, projectId, taskId, ...data } = input; const task = await this.taskService.updateTask(workspaceId, projectId, taskId, data); return { success: true, data: task }; }, }, { name: 'mark_task_done', category: 'task', priority: 4, description: 'Mark a task as done', inputSchema: schemas.workspaceIdSchema.extend({ projectId: schemas.objectIdSchema.describe('The project ID'), taskId: schemas.objectIdSchema.describe('The task ID'), }), handler: async (input: any) => { const task = await this.taskService.markTaskAsDone( input.workspaceId, input.projectId, input.taskId ); return { success: true, data: task }; }, }, { name: 'delete_task', category: 'task', priority: 5, description: 'Delete a task from a project', inputSchema: schemas.workspaceIdSchema.extend({ projectId: schemas.objectIdSchema.describe('The project ID'), taskId: schemas.objectIdSchema.describe('The task ID to delete'), }), handler: async (input: any) => { // Strong protection: don't allow deleting tasks from configured projects if (this.isConfiguredProject(input.projectId)) { throw new Error( 'Cannot delete tasks from a configured project. Tasks in configured projects may be essential for required workflows.' ); } await this.taskService.deleteTask(input.workspaceId, input.projectId, input.taskId); return { success: true, message: 'Task deleted successfully' }; }, }, // Report Tools { name: 'get_summary_report', category: 'report', priority: 1, description: 'Generate a summary report', inputSchema: schemas.reportRequestSchema, handler: async (input: z.infer<typeof schemas.reportRequestSchema>) => { const { workspaceId, userIds, projectIds, clientIds, tagIds, groupBy, ...request } = input; const reportRequest: any = { dateRangeStart: request.dateRangeStart, dateRangeEnd: request.dateRangeEnd, billable: request.billable, }; if (userIds) reportRequest.users = { ids: userIds, contains: 'CONTAINS' }; if (projectIds) reportRequest.projects = { ids: projectIds, contains: 'CONTAINS' }; if (clientIds) reportRequest.clients = { ids: clientIds, contains: 'CONTAINS' }; if (tagIds) reportRequest.tags = { ids: tagIds, contains: 'CONTAINS' }; if (groupBy) reportRequest.summaryFilter = { groups: groupBy }; const report = await this.reportService.getSummaryReport(workspaceId, reportRequest); return { success: true, data: report }; }, }, { name: 'get_user_productivity_report', category: 'report', priority: 2, description: 'Get productivity report for a specific user', inputSchema: schemas.workspaceIdSchema.extend({ userId: z.string().describe('The user ID'), start: z.string().describe('Start date in ISO format'), end: z.string().describe('End date in ISO format'), }), handler: async (input: any) => { const report = await this.reportService.getUserProductivityReport( input.workspaceId, input.userId, { start: input.start, end: input.end } ); return { success: true, data: report }; }, }, { name: 'get_project_progress_report', category: 'report', priority: 3, description: 'Get progress report for a specific project', inputSchema: schemas.workspaceIdSchema.extend({ projectId: schemas.objectIdSchema.describe('The project ID'), start: z.string().describe('Start date in ISO format'), end: z.string().describe('End date in ISO format'), }), handler: async (input: any) => { const report = await this.reportService.getProjectProgressReport( input.workspaceId, input.projectId, { start: input.start, end: input.end } ); return { success: true, data: report }; }, }, { name: 'get_team_utilization_report', category: 'report', priority: 4, description: 'Get team utilization report', inputSchema: schemas.workspaceIdSchema.extend({ start: z.string().describe('Start date in ISO format'), end: z.string().describe('End date in ISO format'), }), handler: async (input: any) => { const report = await this.reportService.getTeamUtilizationReport(input.workspaceId, { start: input.start, end: input.end, }); return { success: true, data: report }; }, }, { name: 'export_report', category: 'report', priority: 5, description: 'Export a report in various formats', inputSchema: schemas.reportRequestSchema.extend({ format: z.enum(['CSV', 'PDF', 'EXCEL']).describe('Export format'), }), handler: async (input: any) => { const { workspaceId, format, userIds, projectIds, clientIds, tagIds, groupBy, ...request } = input; const reportRequest: any = { dateRangeStart: request.dateRangeStart, dateRangeEnd: request.dateRangeEnd, billable: request.billable, }; if (userIds) reportRequest.users = { ids: userIds, contains: 'CONTAINS' }; if (projectIds) reportRequest.projects = { ids: projectIds, contains: 'CONTAINS' }; if (clientIds) reportRequest.clients = { ids: clientIds, contains: 'CONTAINS' }; if (tagIds) reportRequest.tags = { ids: tagIds, contains: 'CONTAINS' }; if (groupBy) reportRequest.summaryFilter = { groups: groupBy }; const report = await this.reportService.exportReport(workspaceId, format, reportRequest); return { success: true, data: report }; }, }, // Custom Field Tools { name: 'get_custom_fields', category: 'customField', priority: 1, description: 'Get all custom fields for a workspace', inputSchema: schemas.workspaceIdSchema, handler: async (input: z.infer<typeof schemas.workspaceIdSchema>) => { const customFields = await this.customFieldService.getAllCustomFields(input.workspaceId); return { success: true, data: customFields }; }, }, { name: 'create_custom_field', category: 'customField', priority: 2, description: 'Create a new custom field', inputSchema: schemas.createCustomFieldSchema, handler: async (input: z.infer<typeof schemas.createCustomFieldSchema>) => { const { workspaceId, ...data } = input; const customField = await this.customFieldService.createCustomField(workspaceId, data); return { success: true, data: customField }; }, }, { name: 'update_custom_field', category: 'customField', priority: 3, description: 'Update an existing custom field', inputSchema: schemas.updateCustomFieldSchema, handler: async (input: z.infer<typeof schemas.updateCustomFieldSchema>) => { const { workspaceId, customFieldId, ...data } = input; const customField = await this.customFieldService.updateCustomField( workspaceId, customFieldId, data ); return { success: true, data: customField }; }, }, { name: 'delete_custom_field', category: 'customField', priority: 4, description: 'Delete a custom field', inputSchema: schemas.workspaceIdSchema.extend({ customFieldId: schemas.objectIdSchema.describe('The custom field ID to delete'), }), handler: async (input: any) => { await this.customFieldService.deleteCustomField(input.workspaceId, input.customFieldId); return { success: true, message: 'Custom field deleted successfully' }; }, }, { name: 'get_custom_field_by_id', category: 'customField', priority: 5, description: 'Get a specific custom field by ID', inputSchema: schemas.workspaceIdSchema.extend({ customFieldId: schemas.objectIdSchema.describe('The custom field ID'), }), handler: async (input: any) => { const customField = await this.customFieldService.getCustomFieldById( input.workspaceId, input.customFieldId ); return { success: true, data: customField }; }, }, ]; } private filterTools(allTools: ToolDefinition[]): ToolDefinition[] { const filtering = this.config.getToolFiltering(); // If specific tools are enabled, only include those if (filtering.enabledTools && filtering.enabledTools.length > 0) { const enabledSet = new Set(filtering.enabledTools); return allTools.filter(tool => enabledSet.has(tool.name)).slice(0, filtering.maxTools); } // Filter by categories const enabledCategories = new Set(filtering.enabledCategories); let filteredTools = allTools.filter(tool => enabledCategories.has(tool.category as any)); // Remove disabled tools if (filtering.disabledTools && filtering.disabledTools.length > 0) { const disabledSet = new Set(filtering.disabledTools); filteredTools = filteredTools.filter(tool => !disabledSet.has(tool.name)); } // Sort by priority (lower number = higher priority) filteredTools.sort((a, b) => (a.priority || 99) - (b.priority || 99)); // Limit to max tools return filteredTools.slice(0, filtering.maxTools); } getTools() { const allTools = this.getAllTools(); const filteredTools = this.filterTools(allTools); return filteredTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, handler: tool.handler, })); } getToolCategories(): string[] { return [ 'user', 'workspace', 'project', 'client', 'timeEntry', 'tag', 'task', 'report', 'bulk', 'search', 'customField', ]; } getAvailableToolNames(): string[] { return this.getAllTools().map(tool => tool.name); } getToolsByCategory(category: string): string[] { return this.getAllTools() .filter(tool => tool.category === category) .map(tool => tool.name); } }

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/hongkongkiwi/clockify-master-mcp'

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