UNPKG

@hongkongkiwi/clockify-master-mcp

Version:

Clockify Master MCP - The most comprehensive Model Context Protocol server for Clockify time tracking with full API integration, advanced filtering, and enterprise features

638 lines 29.8 kB
import { z } from 'zod'; import { ClockifyApiClient, UserService, WorkspaceService, ProjectService, ClientService, TimeEntryService, TagService, TaskService, ReportService, } from '../api/services/index.js'; import * as schemas from './schemas.js'; import { RestrictionMiddleware } from '../middleware/restrictions.js'; export class ClockifyTools { userService; workspaceService; projectService; clientService; timeEntryService; tagService; taskService; reportService; restrictionMiddleware; config; constructor(apiKey, config) { 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.restrictionMiddleware = new RestrictionMiddleware(config); this.config = config; } getAllTools() { 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) => { 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) => { 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) => { 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) => { const workspace = await this.workspaceService.getWorkspaceById(input.workspaceId); return { success: true, data: workspace }; }, }, // 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) => { 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) => { 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) => { 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) => { const { workspaceId, projectId, ...data } = input; 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) => { const project = await this.projectService.archiveProject(input.workspaceId, input.projectId); return { success: true, data: project }; }, }, { 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) => { 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) => { 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) => { 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) => { 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) => { const { workspaceId, clientId, ...data } = input; const client = await this.clientService.updateClient(workspaceId, clientId, data); return { success: true, data: client }; }, }, // 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) => { 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) => { const { workspaceId, timeEntryId, ...data } = input; 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) => { 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) => { 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) => { const entry = await this.timeEntryService.getRunningTimeEntry(input.workspaceId, input.userId); return { success: true, data: entry }; }, }, { name: 'stop_timer', category: 'timeEntry', priority: 6, description: 'Stop the currently running timer', inputSchema: schemas.stopTimerSchema, handler: async (input) => { 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) => { 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) => { 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) => { 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) => { const { workspaceId, timeEntryIds, action, updates } = input; 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) => { 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) => { 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) => { const tags = await this.tagService.createMultipleTags(input.workspaceId, input.names); return { success: true, data: tags }; }, }, // 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) => { 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) => { 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) => { 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) => { const task = await this.taskService.markTaskAsDone(input.workspaceId, input.projectId, input.taskId); return { success: true, data: task }; }, }, // Report Tools { name: 'get_summary_report', category: 'report', priority: 1, description: 'Generate a summary report', inputSchema: schemas.reportRequestSchema, handler: async (input) => { const { workspaceId, userIds, projectIds, clientIds, tagIds, groupBy, ...request } = input; const reportRequest = { 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) => { 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) => { 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) => { 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) => { const { workspaceId, format, userIds, projectIds, clientIds, tagIds, groupBy, ...request } = input; const reportRequest = { 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 }; }, }, ]; } filterTools(allTools) { 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)); // 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() { return [ 'user', 'workspace', 'project', 'client', 'timeEntry', 'tag', 'task', 'report', 'bulk', 'search', ]; } getAvailableToolNames() { return this.getAllTools().map(tool => tool.name); } getToolsByCategory(category) { return this.getAllTools() .filter(tool => tool.category === category) .map(tool => tool.name); } } //# sourceMappingURL=index.js.map