UNPKG

@asanstefanski/everhour-mcp-server

Version:

Complete Everhour API integration for Model Context Protocol (MCP) with 100% endpoint coverage

446 lines (443 loc) 15 kB
import axios from 'axios'; export class EverHourApiClient { client; apiKey; baseUrl; constructor(apiKey, baseUrl = 'https://api.everhour.com') { this.apiKey = apiKey; this.baseUrl = baseUrl; this.client = axios.create({ baseURL: baseUrl, headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json', }, timeout: 30000, }); this.client.interceptors.response.use((response) => response, (error) => { if (error.response?.data) { const apiError = { message: error.response.data.message || 'Unknown API error', code: error.response.data.code || 'UNKNOWN_ERROR', details: error.response.data.details || {}, }; throw new Error(`Everhour API Error: ${apiError.message} (${apiError.code})`); } throw error; }); } // Projects async getProjects(params) { const response = await this.client.get('/projects', { params, }); return response.data; } async getProject(id) { const response = await this.client.get(`/projects/${id}`); return response.data; } async createProject(params) { const response = await this.client.post('/projects', params); return response.data; } async updateProject(id, params) { const response = await this.client.put(`/projects/${id}`, params); return response.data; } async deleteProject(id) { await this.client.delete(`/projects/${id}`); } // Tasks async getTasks(params) { // /tasks/search requires at least a query parameter if (!params?.query && !params?.project) { // If no search criteria provided, return empty array instead of error return []; } const response = await this.client.get('/tasks/search', { params, }); return response.data; } async getTask(id) { const response = await this.client.get(`/tasks/${id}`); return response.data; } async createTask(projectId, params) { const response = await this.client.post(`/projects/${projectId}/tasks`, params); return response.data; } async updateTask(id, params) { const response = await this.client.put(`/tasks/${id}`, params); return response.data; } async deleteTask(id) { await this.client.delete(`/tasks/${id}`); } async getTasksForProject(projectId, params) { const response = await this.client.get(`/projects/${projectId}/tasks`, { params, }); return response.data; } async updateTaskEstimate(id, estimate) { const response = await this.client.put(`/tasks/${id}/estimate`, { estimate }); return response.data; } async deleteTaskEstimate(id) { await this.client.delete(`/tasks/${id}/estimate`); } async addTimeToTask(id, timeParams) { const response = await this.client.post(`/tasks/${id}/time`, timeParams); return response.data; } async updateTaskTime(id, timeParams) { const response = await this.client.put(`/tasks/${id}/time`, timeParams); return response.data; } async deleteTaskTime(id) { await this.client.delete(`/tasks/${id}/time`); } async getTaskTime(id) { const response = await this.client.get(`/tasks/${id}/time`); return response.data; } async getProjectTime(id) { const response = await this.client.get(`/projects/${id}/time`); return response.data; } async getUserTime(id, params) { const response = await this.client.get(`/users/${id}/time`, { params, }); return response.data; } // Time Records async getTimeRecords(params) { const response = await this.client.get('/team/time', { params, }); return response.data; } async createTimeRecord(params) { const response = await this.client.post('/time', params); return response.data; } async updateTimeRecord(id, params) { const response = await this.client.put(`/time/${id}`, params); return response.data; } async deleteTimeRecord(id) { await this.client.delete(`/time/${id}`); } async getTimeRecord(id) { const response = await this.client.get(`/time/${id}`); return response.data; } // Timers (corrected endpoints based on API docs) async getCurrentTimer() { try { const response = await this.client.get('/timers/current'); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 404) { return null; // No active timer } throw error; } } async getRunningTimer() { // Alias for getCurrentTimer for backward compatibility return this.getCurrentTimer(); } async getAllTeamTimers() { const response = await this.client.get('/timers'); return response.data; } async startTimer(params) { const response = await this.client.post('/timers', params); return response.data; } async startTimerForTask(taskId, comment) { const response = await this.client.post('/timers', { task: taskId, comment }); return response.data; } async stopTimer(timerId) { // If no timer ID provided, get current timer first if (!timerId) { const currentTimer = await this.getCurrentTimer(); if (!currentTimer || !currentTimer.id) { throw new Error('No active timer found to stop'); } timerId = Number(currentTimer.id); } const response = await this.client.post(`/timers/${timerId}/stop`); return response.data; } // Legacy timer endpoints (keeping for backward compatibility) async getTimers(params) { const response = await this.client.get('/timers', { params, }); return response.data; } // Clients async getClients(params) { const response = await this.client.get('/clients', { params, }); return response.data; } async getClient(id) { const response = await this.client.get(`/clients/${id}`); return response.data; } async createClient(params) { const response = await this.client.post('/clients', params); return response.data; } async updateClient(id, params) { const response = await this.client.put(`/clients/${id}`, params); return response.data; } async deleteClient(id) { await this.client.delete(`/clients/${id}`); } // Users async getUsers(params) { const response = await this.client.get('/team/users', { params, }); return response.data; } async getCurrentUser() { const response = await this.client.get('/users/me'); return response.data; } // Timecards (Clock In/Out) async getTimecards(params) { const response = await this.client.get('/timecards', { params, }); return response.data; } async getTimecard(id) { const response = await this.client.get(`/timecards/${id}`); return response.data; } async getUserTimecards(userId, params) { const response = await this.client.get(`/users/${userId}/timecards`, { params, }); return response.data; } async updateTimecard(id, params) { const response = await this.client.put(`/timecards/${id}`, params); return response.data; } async deleteTimecard(id) { await this.client.delete(`/timecards/${id}`); } async clockIn(userId, date) { const response = await this.client.post('/timecards/clock-in', { user: userId, date: date || new Date().toISOString().split('T')[0] }); return response.data; } async clockOut(userId) { const response = await this.client.post('/timecards/clock-out', { user: userId }); return response.data; } // Invoices async getInvoices(params) { const response = await this.client.get('/invoices', { params, }); return response.data; } async getInvoice(id) { const response = await this.client.get(`/invoices/${id}`); return response.data; } async createInvoice(params) { const response = await this.client.post('/invoices', params); return response.data; } async updateInvoice(id, params) { const response = await this.client.put(`/invoices/${id}`, params); return response.data; } async deleteInvoice(id) { await this.client.delete(`/invoices/${id}`); } async refreshInvoiceLineItems(id) { const response = await this.client.post(`/invoices/${id}/refresh`); return response.data; } async updateInvoiceStatus(id, status) { const response = await this.client.put(`/invoices/${id}/status`, { status }); return response.data; } async exportInvoice(id, system) { const response = await this.client.post(`/invoices/${id}/export`, { system }); return response.data; } // Expenses async getExpenses(params) { const response = await this.client.get('/expenses', { params, }); return response.data; } async createExpense(params) { const response = await this.client.post('/expenses', params); return response.data; } async updateExpense(id, params) { const response = await this.client.put(`/expenses/${id}`, params); return response.data; } async deleteExpense(id) { await this.client.delete(`/expenses/${id}`); } async getExpenseCategories() { const response = await this.client.get('/expenses/categories'); return response.data; } async createExpenseCategory(name) { const response = await this.client.post('/expenses/categories', { name }); return response.data; } async updateExpenseCategory(id, name) { const response = await this.client.put(`/expenses/categories/${id}`, { name }); return response.data; } async deleteExpenseCategory(id) { await this.client.delete(`/expenses/categories/${id}`); } async createExpenseAttachment(file) { const response = await this.client.post('/expenses/attachments', file, { headers: { 'Content-Type': 'multipart/form-data' } }); return response.data; } async addAttachmentToExpense(expenseId, attachmentId) { const response = await this.client.post(`/expenses/${expenseId}/attachments`, { attachmentId }); return response.data; } // Schedule/Resource Planning - These endpoints don't exist in the Everhour API // Commenting out until/unless they become available /* async getScheduleAssignments(params?: ListParams): Promise<any[]> { throw new Error('Schedule/Resource Planning endpoints are not available in the Everhour API'); } async createScheduleAssignment(params: any): Promise<any> { throw new Error('Schedule/Resource Planning endpoints are not available in the Everhour API'); } async updateScheduleAssignment(id: number, params: any): Promise<any> { throw new Error('Schedule/Resource Planning endpoints are not available in the Everhour API'); } async deleteScheduleAssignment(id: number): Promise<void> { throw new Error('Schedule/Resource Planning endpoints are not available in the Everhour API'); } */ // Sections async getAllSections(params) { // The global /sections endpoint doesn't exist, so we need to get sections from all projects // Limit the number of projects to check to avoid timeouts const projects = await this.getProjects({ limit: 20 }); const allSections = []; // Process projects in smaller batches to avoid timeouts for (let i = 0; i < Math.min(projects.length, 10); i++) { const project = projects[i]; try { const projectSections = await this.getSections(project.id, params); allSections.push(...projectSections); } catch (error) { // Skip projects without sections or with permission issues continue; } } return allSections; } async getSections(projectId, params) { const response = await this.client.get(`/projects/${projectId}/sections`, { params, }); return response.data; } async getSection(id) { const response = await this.client.get(`/sections/${id}`); return response.data; } async createSection(params) { const response = await this.client.post('/sections', params); return response.data; } async updateSection(id, params) { const response = await this.client.put(`/sections/${id}`, params); return response.data; } async deleteSection(id) { await this.client.delete(`/sections/${id}`); } // Utility methods async testConnection() { try { await this.getCurrentUser(); return true; } catch (error) { return false; } } formatTime(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = seconds % 60; if (hours > 0) { return `${hours}h ${minutes}m ${remainingSeconds}s`; } else if (minutes > 0) { return `${minutes}m ${remainingSeconds}s`; } else { return `${remainingSeconds}s`; } } parseTimeToSeconds(timeString) { const timeRegex = /(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+)s)?/; const match = timeString.match(timeRegex); if (!match) { throw new Error('Invalid time format. Use format like "1h 30m 45s", "90m", or "3600s"'); } const hours = parseInt(match[1] || '0', 10); const minutes = parseInt(match[2] || '0', 10); const seconds = parseInt(match[3] || '0', 10); return hours * 3600 + minutes * 60 + seconds; } } //# sourceMappingURL=everhour-client.js.map