@asanstefanski/everhour-mcp-server
Version:
Complete Everhour API integration for Model Context Protocol (MCP) with 100% endpoint coverage
446 lines (443 loc) • 15 kB
JavaScript
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