Skip to main content
Glama
index.ts53.9 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; interface ClockifyConfig { apiKey: string; baseUrl: string; } interface TimeEntry { id?: string; description?: string; start: string; end?: string; projectId?: string; taskId?: string; tagIds?: string[]; billable?: boolean; } interface Project { id?: string; name: string; clientId?: string; workspaceId: string; isPublic?: boolean; billable?: boolean; color?: string; estimate?: { estimate: string; type: "AUTO" | "MANUAL"; }; } interface Task { id?: string; name: string; projectId: string; assigneeIds?: string[]; estimate?: string; status?: "ACTIVE" | "DONE"; } interface Client { id?: string; name: string; workspaceId: string; archived?: boolean; } interface User { id: string; email: string; name: string; memberships: Array<{ userId: string; hourlyRate?: { amount: number; currency: string; }; costRate?: { amount: number; currency: string; }; targetId: string; membershipType: "WORKSPACE" | "PROJECT"; membershipStatus: "PENDING" | "ACTIVE" | "DECLINED" | "INACTIVE"; }>; profilePicture?: string; activeWorkspace: string; defaultWorkspace: string; settings: { weekStart: string; timeZone: string; timeFormat: string; dateFormat: string; sendNewsletter: boolean; weeklyUpdates: boolean; longRunning: boolean; timeTrackingManual: boolean; summaryReportSettings: { group: string; subgroup: string; }; isCompactViewOn: boolean; dashboardSelection: string; dashboardViewType: string; dashboardPinToTop: boolean; projectListCollapse: number; collapseAllProjectLists: boolean; groupSimilarEntriesDisabled: boolean; myStartOfDay: string; projectPickerTaskFilter: boolean; lang: string; multiFactorEnabled: boolean; theme: string; scheduling: boolean; onboarding: boolean; pto: boolean; }; status: string; customFields: Array<{ customFieldId: string; sourceType: string; value: string; }>; } interface Workspace { id: string; name: string; hourlyRate?: { amount: number; currency: string; }; memberships: Array<{ userId: string; hourlyRate?: { amount: number; currency: string; }; costRate?: { amount: number; currency: string; }; targetId: string; membershipType: "WORKSPACE" | "PROJECT"; membershipStatus: "PENDING" | "ACTIVE" | "DECLINED" | "INACTIVE"; }>; workspaceSettings: { timeRoundingInReports: boolean; onlyAdminsSeeBillableRates: boolean; onlyAdminsCreateProject: boolean; onlyAdminsSeeDashboard: boolean; defaultBillableProjects: boolean; lockTimeEntries?: string; round: { round: string; minutes: string; }; projectFavorites: boolean; canSeeTimeSheet: boolean; canSeeTracker: boolean; projectPickerSpecialFilter: boolean; forceProjects: boolean; forceTasks: boolean; forceTags: boolean; forceDescription: boolean; onlyAdminsSeeAllTimeEntries: boolean; onlyAdminsSeePublicProjectsEntries: boolean; trackTimeDownToSecond: boolean; projectGroupingLabel: string; adminOnlyPages: string[]; automaticLock?: { changeDay: string; dayOfMonth: number; firstDay: string; olderThanPeriod: string; olderThanValue: number; type: string; }; onlyAdminsCreateTag: boolean; onlyAdminsCreateTask: boolean; timeTrackingMode: string; isProjectPublicByDefault: boolean; }; imageUrl?: string; featureSubscriptionType?: string; } class ClockifyMCPServer { private server: Server; private config: ClockifyConfig; constructor() { this.config = { apiKey: process.env.CLOCKIFY_API_KEY || "", baseUrl: "https://api.clockify.me/api/v1", }; this.server = new Server( { name: "clockify-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); } private async makeRequest( endpoint: string, method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET", data?: any, baseUrl?: string ): Promise<any> { if (!this.config.apiKey) { throw new McpError( ErrorCode.InvalidParams, "Clockify API key not configured. Set CLOCKIFY_API_KEY environment variable." ); } const url = `${baseUrl || this.config.baseUrl}${endpoint}`; const headers: Record<string, string> = { "X-Api-Key": this.config.apiKey, "Content-Type": "application/json", }; try { const response = await fetch(url, { method, headers, body: data ? JSON.stringify(data) : undefined, }); if (!response.ok) { const errorText = await response.text(); throw new McpError( ErrorCode.InternalError, `Clockify API error (${response.status}): ${errorText}` ); } return response.json(); } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Request failed: ${error instanceof Error ? error.message : String(error)}` ); } } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ // User & Workspace Management { name: "get_current_user", description: "Get information about the current user", inputSchema: { type: "object", properties: {}, }, }, { name: "get_workspaces", description: "Get all workspaces for the current user", inputSchema: { type: "object", properties: {}, }, }, { name: "get_workspace_users", description: "Get all users in a workspace", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, }, required: ["workspaceId"], }, }, // Time Entry Management { name: "create_time_entry", description: "Create a new time entry", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, description: { type: "string", description: "Time entry description" }, start: { type: "string", description: "Start time (ISO 8601 format)" }, end: { type: "string", description: "End time (ISO 8601 format, optional for ongoing entries)" }, projectId: { type: "string", description: "Project ID (optional)" }, taskId: { type: "string", description: "Task ID (optional)" }, tagIds: { type: "array", items: { type: "string" }, description: "Array of tag IDs (optional)" }, billable: { type: "boolean", description: "Whether the entry is billable (optional)" }, }, required: ["workspaceId", "start"], }, }, { name: "get_time_entries", description: "Get time entries for a user", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, userId: { type: "string", description: "User ID (optional, defaults to current user)" }, description: { type: "string", description: "Filter by description" }, start: { type: "string", description: "Start date filter (ISO 8601)" }, end: { type: "string", description: "End date filter (ISO 8601)" }, project: { type: "string", description: "Filter by project ID" }, task: { type: "string", description: "Filter by task ID" }, tags: { type: "string", description: "Filter by tag IDs (comma-separated)" }, projectRequired: { type: "boolean", description: "Filter entries that require project" }, taskRequired: { type: "boolean", description: "Filter entries that require task" }, consideredRunning: { type: "boolean", description: "Include running time entries" }, hydrated: { type: "boolean", description: "Include additional data" }, inProgress: { type: "boolean", description: "Filter by running status" }, page: { type: "number", description: "Page number (default: 1)" }, pageSize: { type: "number", description: "Page size (default: 50, max: 5000)" }, }, required: ["workspaceId"], }, }, { name: "update_time_entry", description: "Update an existing time entry", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, timeEntryId: { type: "string", description: "Time entry ID" }, description: { type: "string", description: "Time entry description" }, start: { type: "string", description: "Start time (ISO 8601 format)" }, end: { type: "string", description: "End time (ISO 8601 format)" }, projectId: { type: "string", description: "Project ID" }, taskId: { type: "string", description: "Task ID" }, tagIds: { type: "array", items: { type: "string" }, description: "Array of tag IDs" }, billable: { type: "boolean", description: "Whether the entry is billable" }, }, required: ["workspaceId", "timeEntryId"], }, }, { name: "delete_time_entry", description: "Delete a time entry", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, timeEntryId: { type: "string", description: "Time entry ID" }, }, required: ["workspaceId", "timeEntryId"], }, }, { name: "stop_time_entry", description: "Stop a running time entry", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, userId: { type: "string", description: "User ID" }, end: { type: "string", description: "End time (ISO 8601 format, optional - defaults to now)" }, }, required: ["workspaceId", "userId"], }, }, { name: "duplicate_time_entry", description: "Duplicate a time entry", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, userId: { type: "string", description: "User ID" }, id: { type: "string", description: "Time entry ID to duplicate" }, }, required: ["workspaceId", "userId", "id"], }, }, // Project Management { name: "create_project", description: "Create a new project", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, name: { type: "string", description: "Project name" }, clientId: { type: "string", description: "Client ID (optional)" }, isPublic: { type: "boolean", description: "Whether project is public (optional)" }, billable: { type: "boolean", description: "Whether project is billable (optional)" }, color: { type: "string", description: "Project color (hex code, optional)" }, estimate: { type: "object", properties: { estimate: { type: "string", description: "Estimate duration (ISO 8601 duration)" }, type: { type: "string", enum: ["AUTO", "MANUAL"], description: "Estimate type" }, }, description: "Project estimate (optional)", }, }, required: ["workspaceId", "name"], }, }, { name: "get_projects", description: "Get all projects in a workspace", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, archived: { type: "boolean", description: "Filter by archived status" }, name: { type: "string", description: "Filter by project name" }, clientIds: { type: "string", description: "Filter by client IDs (comma-separated)" }, containsClient: { type: "boolean", description: "Filter projects that have clients" }, clientStatus: { type: "string", enum: ["ACTIVE", "ARCHIVED"], description: "Filter by client status" }, users: { type: "string", description: "Filter by user IDs (comma-separated)" }, isTemplate: { type: "boolean", description: "Filter by template status" }, sortColumn: { type: "string", description: "Sort column" }, sortOrder: { type: "string", enum: ["ASCENDING", "DESCENDING"], description: "Sort order" }, page: { type: "number", description: "Page number (default: 1)" }, pageSize: { type: "number", description: "Page size (default: 50, max: 5000)" }, }, required: ["workspaceId"], }, }, { name: "get_project", description: "Get a specific project by ID", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, projectId: { type: "string", description: "Project ID" }, }, required: ["workspaceId", "projectId"], }, }, { name: "update_project", description: "Update an existing project", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, projectId: { type: "string", description: "Project ID" }, name: { type: "string", description: "Project name" }, clientId: { type: "string", description: "Client ID" }, isPublic: { type: "boolean", description: "Whether project is public" }, billable: { type: "boolean", description: "Whether project is billable" }, color: { type: "string", description: "Project color (hex code)" }, archived: { type: "boolean", description: "Whether project is archived" }, }, required: ["workspaceId", "projectId"], }, }, { name: "delete_project", description: "Delete a project", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, projectId: { type: "string", description: "Project ID" }, }, required: ["workspaceId", "projectId"], }, }, // Task Management { name: "create_task", description: "Create a new task in a project", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, projectId: { type: "string", description: "Project ID" }, name: { type: "string", description: "Task name" }, assigneeIds: { type: "array", items: { type: "string" }, description: "Array of assignee user IDs (optional)" }, estimate: { type: "string", description: "Task estimate (ISO 8601 duration, optional)" }, status: { type: "string", enum: ["ACTIVE", "DONE"], description: "Task status (optional)" }, }, required: ["workspaceId", "projectId", "name"], }, }, { name: "get_tasks", description: "Get all tasks in a project", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, projectId: { type: "string", description: "Project ID" }, isActive: { type: "boolean", description: "Filter by active status" }, name: { type: "string", description: "Filter by task name" }, page: { type: "number", description: "Page number (default: 1)" }, pageSize: { type: "number", description: "Page size (default: 50, max: 5000)" }, }, required: ["workspaceId", "projectId"], }, }, { name: "get_task", description: "Get a specific task by ID", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, projectId: { type: "string", description: "Project ID" }, taskId: { type: "string", description: "Task ID" }, }, required: ["workspaceId", "projectId", "taskId"], }, }, { name: "update_task", description: "Update an existing task", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, projectId: { type: "string", description: "Project ID" }, taskId: { type: "string", description: "Task ID" }, name: { type: "string", description: "Task name" }, assigneeIds: { type: "array", items: { type: "string" }, description: "Array of assignee user IDs" }, estimate: { type: "string", description: "Task estimate (ISO 8601 duration)" }, status: { type: "string", enum: ["ACTIVE", "DONE"], description: "Task status" }, }, required: ["workspaceId", "projectId", "taskId"], }, }, { name: "delete_task", description: "Delete a task", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, projectId: { type: "string", description: "Project ID" }, taskId: { type: "string", description: "Task ID" }, }, required: ["workspaceId", "projectId", "taskId"], }, }, // Client Management { name: "create_client", description: "Create a new client", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, name: { type: "string", description: "Client name" }, archived: { type: "boolean", description: "Whether client is archived (optional)" }, }, required: ["workspaceId", "name"], }, }, { name: "get_clients", description: "Get all clients in a workspace", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, archived: { type: "boolean", description: "Filter by archived status" }, name: { type: "string", description: "Filter by client name" }, page: { type: "number", description: "Page number (default: 1)" }, pageSize: { type: "number", description: "Page size (default: 50, max: 5000)" }, }, required: ["workspaceId"], }, }, { name: "update_client", description: "Update an existing client", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, clientId: { type: "string", description: "Client ID" }, name: { type: "string", description: "Client name" }, archived: { type: "boolean", description: "Whether client is archived" }, }, required: ["workspaceId", "clientId"], }, }, { name: "delete_client", description: "Delete a client", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, clientId: { type: "string", description: "Client ID" }, }, required: ["workspaceId", "clientId"], }, }, // Tag Management { name: "create_tag", description: "Create a new tag", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, name: { type: "string", description: "Tag name" }, archived: { type: "boolean", description: "Whether tag is archived (optional)" }, }, required: ["workspaceId", "name"], }, }, { name: "get_tags", description: "Get all tags in a workspace", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, archived: { type: "boolean", description: "Filter by archived status" }, name: { type: "string", description: "Filter by tag name" }, page: { type: "number", description: "Page number (default: 1)" }, pageSize: { type: "number", description: "Page size (default: 50, max: 5000)" }, }, required: ["workspaceId"], }, }, { name: "update_tag", description: "Update an existing tag", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, tagId: { type: "string", description: "Tag ID" }, name: { type: "string", description: "Tag name" }, archived: { type: "boolean", description: "Whether tag is archived" }, }, required: ["workspaceId", "tagId"], }, }, { name: "delete_tag", description: "Delete a tag", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, tagId: { type: "string", description: "Tag ID" }, }, required: ["workspaceId", "tagId"], }, }, // Reports { name: "get_detailed_report", description: "Generate a detailed time tracking report", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, dateRangeStart: { type: "string", description: "Start date (ISO 8601 format)" }, dateRangeEnd: { type: "string", description: "End date (ISO 8601 format)" }, users: { type: "array", items: { type: "string" }, description: "Array of user IDs to filter" }, clients: { type: "array", items: { type: "string" }, description: "Array of client IDs to filter" }, projects: { type: "array", items: { type: "string" }, description: "Array of project IDs to filter" }, tasks: { type: "array", items: { type: "string" }, description: "Array of task IDs to filter" }, tags: { type: "array", items: { type: "string" }, description: "Array of tag IDs to filter" }, billable: { type: "boolean", description: "Filter by billable status" }, description: { type: "string", description: "Filter by description" }, withoutDescription: { type: "boolean", description: "Filter entries without description" }, customFieldIds: { type: "array", items: { type: "string" }, description: "Array of custom field IDs" }, sortColumn: { type: "string", description: "Sort column (DATE, USER, PROJECT, etc.)" }, sortOrder: { type: "string", enum: ["ASCENDING", "DESCENDING"], description: "Sort order" }, page: { type: "number", description: "Page number (default: 1)" }, pageSize: { type: "number", description: "Page size (default: 50, max: 1000)" }, exportType: { type: "string", enum: ["JSON", "PDF", "CSV", "XLSX"], description: "Export format" }, }, required: ["workspaceId", "dateRangeStart", "dateRangeEnd"], }, }, { name: "get_summary_report", description: "Generate a summary time tracking report", inputSchema: { type: "object", properties: { workspaceId: { type: "string", description: "Workspace ID" }, dateRangeStart: { type: "string", description: "Start date (ISO 8601 format)" }, dateRangeEnd: { type: "string", description: "End date (ISO 8601 format)" }, users: { type: "array", items: { type: "string" }, description: "Array of user IDs to filter" }, clients: { type: "array", items: { type: "string" }, description: "Array of client IDs to filter" }, projects: { type: "array", items: { type: "string" }, description: "Array of project IDs to filter" }, tasks: { type: "array", items: { type: "string" }, description: "Array of task IDs to filter" }, tags: { type: "array", items: { type: "string" }, description: "Array of tag IDs to filter" }, billable: { type: "boolean", description: "Filter by billable status" }, groups: { type: "array", items: { type: "string" }, description: "Group by fields (USER, PROJECT, CLIENT, etc.)" }, sortColumn: { type: "string", description: "Sort column" }, sortOrder: { type: "string", enum: ["ASCENDING", "DESCENDING"], description: "Sort order" }, exportType: { type: "string", enum: ["JSON", "PDF", "CSV", "XLSX"], description: "Export format" }, }, required: ["workspaceId", "dateRangeStart", "dateRangeEnd"], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { // User & Workspace Management case "get_current_user": return await this.getCurrentUser(); case "get_workspaces": return await this.getWorkspaces(); case "get_workspace_users": if (!args?.workspaceId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required'); return await this.getWorkspaceUsers(args.workspaceId as string); // Time Entry Management case "create_time_entry": if (!args?.workspaceId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required'); return await this.createTimeEntry(args as any); case "get_time_entries": if (!args?.workspaceId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required'); return await this.getTimeEntries(args as any); case "update_time_entry": if (!args?.workspaceId || !args?.timeEntryId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and timeEntryId are required'); return await this.updateTimeEntry(args as any); case "delete_time_entry": if (!args?.workspaceId || !args?.timeEntryId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and timeEntryId are required'); return await this.deleteTimeEntry(args.workspaceId as string, args.timeEntryId as string); case "stop_time_entry": if (!args?.workspaceId || !args?.userId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and userId are required'); return await this.stopTimeEntry(args as any); case "duplicate_time_entry": if (!args?.workspaceId || !args?.userId || !args?.id) throw new McpError(ErrorCode.InvalidParams, 'workspaceId, userId and id are required'); return await this.duplicateTimeEntry(args as any); // Project Management case "create_project": if (!args?.workspaceId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required'); return await this.createProject(args as any); case "get_projects": if (!args?.workspaceId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required'); return await this.getProjects(args as any); case "get_project": if (!args?.workspaceId || !args?.projectId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and projectId are required'); return await this.getProject(args.workspaceId as string, args.projectId as string); case "update_project": if (!args?.workspaceId || !args?.projectId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and projectId are required'); return await this.updateProject(args as any); case "delete_project": if (!args?.workspaceId || !args?.projectId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and projectId are required'); return await this.deleteProject(args.workspaceId as string, args.projectId as string); // Task Management case "create_task": if (!args?.workspaceId || !args?.projectId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and projectId are required'); return await this.createTask(args as any); case "get_tasks": if (!args?.workspaceId || !args?.projectId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and projectId are required'); return await this.getTasks(args as any); case "get_task": if (!args?.workspaceId || !args?.projectId || !args?.taskId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId, projectId and taskId are required'); return await this.getTask(args.workspaceId as string, args.projectId as string, args.taskId as string); case "update_task": if (!args?.workspaceId || !args?.projectId || !args?.taskId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId, projectId and taskId are required'); return await this.updateTask(args as any); case "delete_task": if (!args?.workspaceId || !args?.projectId || !args?.taskId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId, projectId and taskId are required'); return await this.deleteTask(args.workspaceId as string, args.projectId as string, args.taskId as string); // Client Management case "create_client": if (!args?.workspaceId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required'); return await this.createClient(args as any); case "get_clients": if (!args?.workspaceId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required'); return await this.getClients(args as any); case "update_client": if (!args?.workspaceId || !args?.clientId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and clientId are required'); return await this.updateClient(args as any); case "delete_client": if (!args?.workspaceId || !args?.clientId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and clientId are required'); return await this.deleteClient(args.workspaceId as string, args.clientId as string); // Tag Management case "create_tag": if (!args?.workspaceId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required'); return await this.createTag(args as any); case "get_tags": if (!args?.workspaceId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId is required'); return await this.getTags(args as any); case "update_tag": if (!args?.workspaceId || !args?.tagId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and tagId are required'); return await this.updateTag(args as any); case "delete_tag": if (!args?.workspaceId || !args?.tagId) throw new McpError(ErrorCode.InvalidParams, 'workspaceId and tagId are required'); return await this.deleteTag(args.workspaceId as string, args.tagId as string); // Reports case "get_detailed_report": return await this.getDetailedReport(args); case "get_summary_report": return await this.getSummaryReport(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}` ); } }); } // User & Workspace Management Methods private async getCurrentUser() { const user = await this.makeRequest("/user"); return { content: [ { type: "text", text: `Current user: ${user.name} (${user.email})\nActive Workspace: ${user.activeWorkspace}\nUser ID: ${user.id}`, }, ], isError: false, }; } private async getWorkspaces() { const workspaces = await this.makeRequest("/workspaces"); return { content: [ { type: "text", text: `Found ${workspaces.length} workspace(s):\n${workspaces .map((w: Workspace) => `- ${w.name} (${w.id})`) .join("\n")}`, }, ], isError: false, }; } private async getWorkspaceUsers(workspaceId: string | undefined) { const users = await this.makeRequest(`/workspaces/${workspaceId}/users`); return { content: [ { type: "text", text: `Found ${users.length} user(s) in workspace:\n${users .map((u: User) => `- ${u.name} (${u.email}) - ${u.id}`) .join("\n")}`, }, ], isError: false, }; } // Time Entry Management Methods private async createTimeEntry(args: any) { const { workspaceId, ...timeEntryData } = args; // Ensure start time is in ISO format if (!timeEntryData.start.includes("T")) { timeEntryData.start = new Date(timeEntryData.start).toISOString(); } // If end time is provided, ensure it's in ISO format if (timeEntryData.end && !timeEntryData.end.includes("T")) { timeEntryData.end = new Date(timeEntryData.end).toISOString(); } const timeEntry = await this.makeRequest( `/workspaces/${workspaceId}/time-entries`, "POST", timeEntryData ); return { content: [ { type: "text", text: `Time entry created successfully!\nID: ${timeEntry.id}\nDescription: ${timeEntry.description || "No description"}\nStart: ${timeEntry.timeInterval.start}\nEnd: ${timeEntry.timeInterval.end || "Ongoing"}`, }, ], isError: false, }; } private async getTimeEntries(args: any) { const { workspaceId, userId, ...params } = args; // Build query parameters const queryParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { queryParams.append(key, String(value)); } }); const endpoint = userId ? `/workspaces/${workspaceId}/user/${userId}/time-entries` : `/workspaces/${workspaceId}/time-entries`; const fullEndpoint = queryParams.toString() ? `${endpoint}?${queryParams.toString()}` : endpoint; const timeEntries = await this.makeRequest(fullEndpoint); return { content: [ { type: "text", text: `Found ${timeEntries.length} time entries:\n${timeEntries .map((entry: any) => `- ${entry.description || "No description"} | ${entry.timeInterval.start} - ${entry.timeInterval.end || "Ongoing"} | ${entry.timeInterval.duration || "Running"}` ) .join("\n")}`, }, ], isError: false, }; } private async updateTimeEntry(args: any) { const { workspaceId, timeEntryId, ...updateData } = args; // Ensure dates are in ISO format if (updateData.start && !updateData.start.includes("T")) { updateData.start = new Date(updateData.start).toISOString(); } if (updateData.end && !updateData.end.includes("T")) { updateData.end = new Date(updateData.end).toISOString(); } const timeEntry = await this.makeRequest( `/workspaces/${workspaceId}/time-entries/${timeEntryId}`, "PUT", updateData ); return { content: [ { type: "text", text: `Time entry updated successfully!\nID: ${timeEntry.id}\nDescription: ${timeEntry.description || "No description"}\nStart: ${timeEntry.timeInterval.start}\nEnd: ${timeEntry.timeInterval.end || "Ongoing"}`, }, ], isError: false, }; } private async deleteTimeEntry(workspaceId: string, timeEntryId: string) { await this.makeRequest( `/workspaces/${workspaceId}/time-entries/${timeEntryId}`, "DELETE" ); return { content: [ { type: "text", text: `Time entry ${timeEntryId} deleted successfully!`, }, ], isError: false, }; } private async stopTimeEntry(args: any) { const { workspaceId, userId, end } = args; const endTime = end || new Date().toISOString(); const result = await this.makeRequest( `/workspaces/${workspaceId}/user/${userId}/time-entries`, "PATCH" as "PATCH", { end: endTime } ); return { content: [ { type: "text", text: `Time entry stopped at ${endTime}\nDuration: ${result.timeInterval.duration}`, }, ], isError: false, }; } private async duplicateTimeEntry(args: any) { const { workspaceId, userId, id } = args; const result = await this.makeRequest( `/workspaces/${workspaceId}/user/${userId}/time-entries/${id}/duplicate`, "POST" ); return { content: [ { type: "text", text: `Time entry duplicated successfully!\nID: ${result.id}\nDescription: ${result.description || "No description"}\nStart: ${result.timeInterval.start}\nEnd: ${result.timeInterval.end || "Ongoing"}\nDuration: ${result.timeInterval.duration || "N/A"}\nBillable: ${result.billable}\nProject: ${result.projectId || "No project"}`, }, ], isError: false, }; } // Project Management Methods private async createProject(args: any) { const { workspaceId, ...projectData } = args; const project = await this.makeRequest( `/workspaces/${workspaceId}/projects`, "POST", projectData ); return { content: [ { type: "text", text: `Project created successfully!\nID: ${project.id}\nName: ${project.name}\nClient: ${project.clientName || "No client"}\nPublic: ${project.public}\nBillable: ${project.billable}`, }, ], isError: false, }; } private async getProjects(args: any) { const { workspaceId, ...params } = args; const queryParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { queryParams.append(key, String(value)); } }); const endpoint = queryParams.toString() ? `/workspaces/${workspaceId}/projects?${queryParams.toString()}` : `/workspaces/${workspaceId}/projects`; const projects = await this.makeRequest(endpoint); return { content: [ { type: "text", text: `Found ${projects.length} project(s):\n${projects .map((p: any) => `- ${p.name} (${p.id}) | Client: ${p.clientName || "None"} | Billable: ${p.billable}`) .join("\n")}`, }, ], isError: false, }; } private async getProject(workspaceId: string, projectId: string) { const project = await this.makeRequest(`/workspaces/${workspaceId}/projects/${projectId}`); return { content: [ { type: "text", text: `Project Details:\nName: ${project.name}\nID: ${project.id}\nClient: ${project.clientName || "No client"}\nPublic: ${project.public}\nBillable: ${project.billable}\nColor: ${project.color}\nArchived: ${project.archived}`, }, ], isError: false, }; } private async updateProject(args: any) { const { workspaceId, projectId, ...updateData } = args; const project = await this.makeRequest( `/workspaces/${workspaceId}/projects/${projectId}`, "PUT", updateData ); return { content: [ { type: "text", text: `Project updated successfully!\nName: ${project.name}\nClient: ${project.clientName || "No client"}\nBillable: ${project.billable}`, }, ], isError: false, }; } private async deleteProject(workspaceId: string, projectId: string) { await this.makeRequest( `/workspaces/${workspaceId}/projects/${projectId}`, "DELETE" ); return { content: [ { type: "text", text: `Project ${projectId} deleted successfully!`, }, ], isError: false, }; } // Task Management Methods private async createTask(args: any) { const { workspaceId, projectId, ...taskData } = args; const task = await this.makeRequest( `/workspaces/${workspaceId}/projects/${projectId}/tasks`, "POST", taskData ); return { content: [ { type: "text", text: `Task created successfully!\nID: ${task.id}\nName: ${task.name}\nProject: ${projectId}\nStatus: ${task.status}\nEstimate: ${task.estimate || "No estimate"}`, }, ], isError: false, }; } private async getTasks(args: any) { const { workspaceId, projectId, ...params } = args; const queryParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { queryParams.append(key, String(value)); } }); const endpoint = queryParams.toString() ? `/workspaces/${workspaceId}/projects/${projectId}/tasks?${queryParams.toString()}` : `/workspaces/${workspaceId}/projects/${projectId}/tasks`; const tasks = await this.makeRequest(endpoint); return { content: [ { type: "text", text: `Found ${tasks.length} task(s):\n${tasks .map((t: any) => `- ${t.name} (${t.id}) | Status: ${t.status} | Estimate: ${t.estimate || "None"}`) .join("\n")}`, }, ], isError: false, }; } private async getTask(workspaceId: string, projectId: string, taskId: string) { const task = await this.makeRequest( `/workspaces/${workspaceId}/projects/${projectId}/tasks/${taskId}` ); return { content: [ { type: "text", text: `Task Details:\nName: ${task.name}\nID: ${task.id}\nProject: ${projectId}\nStatus: ${task.status}\nEstimate: ${task.estimate || "No estimate"}\nAssignees: ${task.assigneeIds?.length || 0}`, }, ], isError: false, }; } private async updateTask(args: any) { const { workspaceId, projectId, taskId, ...updateData } = args; const task = await this.makeRequest( `/workspaces/${workspaceId}/projects/${projectId}/tasks/${taskId}`, "PUT", updateData ); return { content: [ { type: "text", text: `Task updated successfully!\nName: ${task.name}\nStatus: ${task.status}\nEstimate: ${task.estimate || "No estimate"}`, }, ], isError: false, }; } private async deleteTask(workspaceId: string, projectId: string, taskId: string) { await this.makeRequest( `/workspaces/${workspaceId}/projects/${projectId}/tasks/${taskId}`, "DELETE" ); return { content: [ { type: "text", text: `Task ${taskId} deleted successfully!`, }, ], isError: false, }; } // Client Management Methods private async createClient(args: any) { const { workspaceId, ...clientData } = args; const client = await this.makeRequest( `/workspaces/${workspaceId}/clients`, "POST", clientData ); return { content: [ { type: "text", text: `Client created successfully!\nID: ${client.id}\nName: ${client.name}\nArchived: ${client.archived}`, }, ], isError: false, }; } private async getClients(args: any) { const { workspaceId, ...params } = args; const queryParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { queryParams.append(key, String(value)); } }); const endpoint = queryParams.toString() ? `/workspaces/${workspaceId}/clients?${queryParams.toString()}` : `/workspaces/${workspaceId}/clients`; const clients = await this.makeRequest(endpoint); return { content: [ { type: "text", text: `Found ${clients.length} client(s):\n${clients .map((c: any) => `- ${c.name} (${c.id}) | Archived: ${c.archived}`) .join("\n")}`, }, ], isError: false, }; } private async updateClient(args: any) { const { workspaceId, clientId, ...updateData } = args; const client = await this.makeRequest( `/workspaces/${workspaceId}/clients/${clientId}`, "PUT", updateData ); return { content: [ { type: "text", text: `Client updated successfully!\nName: ${client.name}\nArchived: ${client.archived}`, }, ], isError: false, }; } private async deleteClient(workspaceId: string, clientId: string) { await this.makeRequest( `/workspaces/${workspaceId}/clients/${clientId}`, "DELETE" ); return { content: [ { type: "text", text: `Client ${clientId} deleted successfully!`, }, ], isError: false, }; } // Tag Management Methods private async createTag(args: any) { const { workspaceId, ...tagData } = args; const tag = await this.makeRequest( `/workspaces/${workspaceId}/tags`, "POST", tagData ); return { content: [ { type: "text", text: `Tag created successfully!\nID: ${tag.id}\nName: ${tag.name}\nArchived: ${tag.archived}`, }, ], isError: false, }; } private async getTags(args: any) { const { workspaceId, ...params } = args; const queryParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { queryParams.append(key, String(value)); } }); const endpoint = queryParams.toString() ? `/workspaces/${workspaceId}/tags?${queryParams.toString()}` : `/workspaces/${workspaceId}/tags`; const tags = await this.makeRequest(endpoint); return { content: [ { type: "text", text: `Found ${tags.length} tag(s):\n${tags .map((t: any) => `- ${t.name} (${t.id}) | Archived: ${t.archived}`) .join("\n")}`, }, ], isError: false, }; } private async updateTag(args: any) { const { workspaceId, tagId, ...updateData } = args; const tag = await this.makeRequest( `/workspaces/${workspaceId}/tags/${tagId}`, "PUT", updateData ); return { content: [ { type: "text", text: `Tag updated successfully!\nName: ${tag.name}\nArchived: ${tag.archived}`, }, ], isError: false, }; } private async deleteTag(workspaceId: string, tagId: string) { await this.makeRequest( `/workspaces/${workspaceId}/tags/${tagId}`, "DELETE" ); return { content: [ { type: "text", text: `Tag ${tagId} deleted successfully!`, }, ], isError: false, }; } // Report Methods private async getDetailedReport(args: any) { const { workspaceId, ...reportData } = args; const payload = { dateRangeStart: reportData.dateRangeStart, dateRangeEnd: reportData.dateRangeEnd, detailedFilter: { sortColumn: reportData.sortColumn || "DATE", sortOrder: reportData.sortOrder || "DESCENDING", page: reportData.page || 1, pageSize: Math.min(reportData.pageSize || 50, 1000), options: { totals: "CALCULATE", }, }, users: reportData.users ? { ids: reportData.users } : undefined, clients: reportData.clients ? { ids: reportData.clients } : undefined, projects: reportData.projects ? { ids: reportData.projects } : undefined, tasks: reportData.tasks ? { ids: reportData.tasks } : undefined, tags: reportData.tags ? { ids: reportData.tags } : undefined, billable: reportData.billable, description: reportData.description, withoutDescription: reportData.withoutDescription, customFieldIds: reportData.customFieldIds, sortColumn: reportData.sortColumn, sortOrder: reportData.sortOrder, page: reportData.page, pageSize: reportData.pageSize, exportType: reportData.exportType || "JSON", }; // Remove undefined properties Object.keys(payload).forEach(key => { if (payload[key as keyof typeof payload] === undefined) { delete payload[key as keyof typeof payload]; } }); const report = await this.makeRequest( `/workspaces/${workspaceId}/reports/detailed`, "POST", payload, "https://reports.api.clockify.me/v1" ); const summary = `Detailed Report Summary: Total Entries: ${report.timeentries?.length || 0} Total Duration: ${report.totals?.[0]?.totalTime || "0:00:00"} Date Range: ${reportData.dateRangeStart} to ${reportData.dateRangeEnd}`; return { content: [ { type: "text", text: summary, }, ], isError: false, }; } private async getSummaryReport(args: any) { const { workspaceId, ...reportData } = args; const payload = { dateRangeStart: reportData.dateRangeStart, dateRangeEnd: reportData.dateRangeEnd, summaryFilter: { groups: reportData.groups || ["PROJECT"], sortColumn: reportData.sortColumn || "DURATION", sortOrder: reportData.sortOrder || "DESCENDING", }, users: reportData.users ? { ids: reportData.users } : undefined, clients: reportData.clients ? { ids: reportData.clients } : undefined, projects: reportData.projects ? { ids: reportData.projects } : undefined, tasks: reportData.tasks ? { ids: reportData.tasks } : undefined, tags: reportData.tags ? { ids: reportData.tags } : undefined, billable: reportData.billable, exportType: reportData.exportType || "JSON", }; // Remove undefined properties Object.keys(payload).forEach(key => { if (payload[key as keyof typeof payload] === undefined) { delete payload[key as keyof typeof payload]; } }); const report = await this.makeRequest( `/workspaces/${workspaceId}/reports/summary`, "POST", payload, "https://reports.api.clockify.me/v1" ); const summary = `Summary Report: Groups: ${reportData.groups?.join(", ") || "PROJECT"} Total Duration: ${report.totals?.[0]?.totalTime || "0:00:00"} Date Range: ${reportData.dateRangeStart} to ${reportData.dateRangeEnd} Group Count: ${report.groupOne?.length || 0}`; return { content: [ { type: "text", text: summary, }, ], isError: false, }; } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Clockify MCP server running on stdio"); } } const server = new ClockifyMCPServer(); server.run().catch(console.error);

Implementation Reference

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/ratheesh-aot/clockify-mcp'

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