UNPKG

@clicktime/mcp-server

Version:

ClickTime MCP Tech Demo for AI agents to interact with ClickTime API

230 lines (229 loc) 9.52 kB
// src/api-clients.ts import axios from 'axios'; import { USER_AGENT } from './constants.js'; export class ClickTimeAPIClient { client; readOnly; constructor(config) { this.readOnly = config.readOnly || false; this.client = axios.create({ baseURL: config.baseUrl, headers: { Authorization: `token ${config.apiToken}`, 'Content-Type': 'application/json', 'User-Agent': USER_AGENT, }, timeout: 30_000, }); // Attach interceptor for live HTTP calls this.client.interceptors.response.use((response) => response, (err) => Promise.reject(ClickTimeAPIClient.mapAxiosError(err))); } /** Centralised mapping from Axios errors → friendly messages. */ static mapAxiosError(err) { const resp = err?.response; if (!resp) return err; // network or non-HTTP error, pass through untouched switch (resp.status) { case 401: return new Error('Invalid ClickTime API token. Please check your credentials.'); case 403: return new Error('Access denied. Please check your API token permissions.'); case 429: return new Error('Rate limit exceeded. Please try again later.'); default: { // Prefer server-supplied message when available const msg = resp.data?.message || resp.statusText || 'Unknown ClickTime error'; return new Error(`ClickTime API error: ${msg}`); } } } async makeRequest(method, endpoint, data, params) { if (this.readOnly && ['POST', 'PUT', 'DELETE'].includes(method)) { throw new Error('Write operations are disabled in read-only mode'); } try { const response = await this.client.request({ method, url: endpoint, data, params, }); return response.data; } catch (err) { // Fallback for mocked Axios requests where the interceptor never runs throw ClickTimeAPIClient.mapAxiosError(err); } } /* -------------------------------------------------------------------- * * Time-entry methods * -------------------------------------------------------------------- */ async getMyTimeEntries(params = {}) { // Add verbose parameter to include Job and Task details const enhancedParams = { ...params, verbose: true }; const response = await this.makeRequest('GET', '/Me/TimeEntries', null, enhancedParams); return response.data ?? []; } async createTimeEntry(data) { const response = await this.makeRequest('POST', '/Me/TimeEntries', data); return response.data; } async updateTimeEntry(entryId, data) { const response = await this.makeRequest('PUT', `/Me/TimeEntries/${entryId}`, data); return response.data; } async deleteTimeEntry(entryId) { await this.makeRequest('DELETE', `/Me/TimeEntries/${entryId}`); } /* -------------------------------------------------------------------- * * Job / task methods * -------------------------------------------------------------------- */ async getMyJobs() { const response = await this.makeRequest('GET', '/Me/Jobs'); return response.data ?? []; } async getJobById(jobId) { const response = await this.makeRequest('GET', `/Jobs/${jobId}`); return response.data; } async getMyTasks() { const response = await this.makeRequest('GET', '/Me/Tasks'); return response.data ?? []; } async getTaskById(taskId) { const response = await this.makeRequest('GET', `/Tasks/${taskId}`); return response.data; } /* -------------------------------------------------------------------- * * Time-off methods * -------------------------------------------------------------------- */ async getMyTimeOffRequests(params = {}) { // Add verbose parameter to include full details const enhancedParams = { ...params, verbose: true }; const response = await this.makeRequest('GET', '/Me/TimeOffRequests', null, enhancedParams); return response.data ?? []; } async getTimeOffRequestById(requestId) { const response = await this.makeRequest('GET', `/Me/TimeOffRequests/${requestId}`, null, { verbose: true }); return response.data; } async getTimeOffRequestActions(requestId) { const response = await this.makeRequest('GET', `/Me/TimeOffRequests/${requestId}/Actions`); return response.data ?? []; } async performTimeOffRequestAction(requestId, action, comment) { const response = await this.makeRequest('POST', `/Me/TimeOffRequests/${requestId}/Actions`, { Action: action, Comment: comment }); return response.data; } async createTimeOffRequest(data) { const response = await this.makeRequest('POST', '/Me/TimeOffRequests', data); return response.data; } async getMyTimeOff(params = {}) { // Add verbose parameter to include full details const enhancedParams = { ...params, verbose: true }; const response = await this.makeRequest('GET', '/Me/TimeOff', null, enhancedParams); return response.data ?? []; } async createTimeOff(data) { const response = await this.makeRequest('POST', '/Me/TimeOff', data); return response.data; } async deleteTimeOff(timeOffId, dcaaExplanation) { const params = dcaaExplanation ? { DCAAExplanation: dcaaExplanation } : undefined; await this.makeRequest('DELETE', `/Me/TimeOff/${timeOffId}`, null, params); } async getTimeOffTypes() { const response = await this.makeRequest('GET', '/Me/TimeOffTypes', null, { verbose: true }); return response.data ?? []; } async getTimeOffBalance(typeId) { const response = await this.makeRequest('GET', `/Me/TimeOffTypes/${typeId}`, null, { verbose: true }); return response.data; } /* -------------------------------------------------------------------- * * Expense-type & payment-type methods * -------------------------------------------------------------------- */ async getMyExpenseTypes() { const response = await this.makeRequest('GET', '/ExpenseTypes'); return response.data ?? []; } async getExpenseTypeById(expenseTypeId) { const response = await this.makeRequest('GET', `/ExpenseTypes/${expenseTypeId}`); return response.data; } async getMyPaymentTypes() { const response = await this.makeRequest('GET', '/PaymentTypes'); return response.data ?? []; } async getPaymentTypeById(paymentTypeId) { const response = await this.makeRequest('GET', `/PaymentTypes/${paymentTypeId}`); return response.data; } /* -------------------------------------------------------------------- * * Expense-sheet & item methods * -------------------------------------------------------------------- */ async getMyExpenseSheets(params = {}) { const response = await this.makeRequest('GET', '/Me/ExpenseSheets', null, params); return response.data ?? []; } async createExpenseSheet(data) { const response = await this.makeRequest('POST', '/Me/ExpenseSheets', data); return response.data; } async getMyExpenseItems(params = {}) { const response = await this.makeRequest('GET', '/Me/ExpenseItems', null, params); return response.data ?? []; } async createExpenseItem(data) { const response = await this.makeRequest('POST', '/Me/ExpenseItems', data); return response.data; } /* -------------------------------------------------------------------- * * User / health-check methods * -------------------------------------------------------------------- */ async getMe() { const response = await this.makeRequest('GET', '/Me'); return response.data; } async healthCheck() { try { await this.getMe(); return true; } catch { return false; } } /* -------------------------------------------------------------------- * * Timesheet submission methods * -------------------------------------------------------------------- */ async getMyTimesheets(params = {}) { const response = await this.makeRequest('GET', '/Me/Timesheets', null, params); return Array.isArray(response.data) ? response.data : []; } async submitTimesheetById(timesheetId, actionData) { if (!timesheetId?.trim()) throw new Error('Timesheet ID is required'); const response = await this.makeRequest('POST', `/Me/Timesheets/${timesheetId}/Actions`, actionData); return response.data; } async submitExpenseSheet(expenseSheetId, actionData) { if (!expenseSheetId?.trim()) throw new Error('Expense sheet ID is required'); const response = await this.makeRequest('POST', `/Me/ExpenseSheets/${expenseSheetId}/Actions`, actionData); return response.data; } }