Skip to main content
Glama

TimeCamp MCP Server

Official
by timecamp-org
timecamp-api.ts11.4 kB
// TimeCamp API client class export class TimeCampAPI { private baseUrl = "https://app.timecamp.com/third_party/api"; private bearerToken: string; constructor(bearerToken: string) { this.bearerToken = bearerToken; // Validate bearer token if (!this.bearerToken) { throw new Error("No bearer token provided. Please set the TIMECAMP_TOKEN environment variable or provide Authorization header."); } } private getHeaders() { return { "Accept": "application/json", "Authorization": `Bearer ${this.bearerToken}`, "Content-Type": "application/json", }; } private calculateDuration(startTime: string, endTime: string): number { const start = new Date(startTime); const end = new Date(endTime); return Math.floor((end.getTime() - start.getTime()) / 1000); // Duration in seconds } private formatDateForAPI(dateTime: string): string { // Convert "2025-06-22 13:28" format to "2025-06-22 13:28:00" if (dateTime.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)) { return dateTime + ":00"; } return dateTime; } async createTimeEntry(from: string, to: string, note: string, task_id?: string) { try { // Format and validate dates const startTime = this.formatDateForAPI(from); const endTime = this.formatDateForAPI(to); const duration = this.calculateDuration(startTime, endTime); if (duration <= 0) { throw new Error("End time must be after start time"); } // Extract date for the API (YYYY-MM-DD format) const date = from.split(' ')[0]; // Prepare request body const requestBody: any = { get_entries: 0, date: date, start_time: startTime, end_time: endTime, duration: duration, note: note, service: 'timecamp-mcp', }; // Add task_id if provided if (task_id) { requestBody.task_id = task_id; } // Make API request const response = await fetch(`${this.baseUrl}/entries`, { method: "POST", headers: this.getHeaders(), body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`TimeCamp API request failed with status ${response.status}: ${errorText}`); } const result = await response.json(); return { success: true, duration: Math.floor(duration / 60), // duration in minutes data: result, message: `Successfully created time entry from ${from} to ${to} (${Math.floor(duration / 60)} minutes).` }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } async getTimeEntries(fromDate: string, toDate: string, userIds = "me", optFields = "tags") { try { // Prepare query parameters const params = new URLSearchParams({ from: fromDate, to: toDate, user_ids: userIds, opt_fields: optFields, }); // Make API request const response = await fetch(`${this.baseUrl}/entries?${params.toString()}`, { method: "GET", headers: this.getHeaders(), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`TimeCamp API request failed with status ${response.status}: ${errorText}`); } const result = await response.json(); // Process the response to remove unwanted fields and rename description to note const processedData = Array.isArray(result) ? result.map(entry => { const { task_note, locked, addons_external_id, color, description, hasEntryLocationHistory, name, duration, ...cleanEntry } = entry; // Calculate duration in hours const durationSeconds = parseInt(duration) || 0; const durationHours = (durationSeconds / 3600).toFixed(2); return { ...cleanEntry, task_name: name || "", duration_seconds: durationSeconds, duration_hours: parseFloat(durationHours), note: description || "" }; }) : result; return { success: true, data: processedData, message: `Time entries from ${fromDate} to ${toDate}` }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } async getTasks() { try { // Prepare query parameters const params = new URLSearchParams({ ignoreAdminRights: "1" }); // Make API request const response = await fetch(`${this.baseUrl}/tasks?${params.toString()}`, { method: "GET", headers: this.getHeaders(), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`TimeCamp API request failed with status ${response.status}: ${errorText}`); } const result = await response.json(); // Convert object to array, filter non-archived tasks, and return only specified fields const tasksArray = Object.values(result as Record<string, any>) .filter((task: any) => task.archived == 0) .map((task: any) => ({ task_id: task.task_id, parent_id: task.parent_id, name: task.name, level: task.level, note: task.note })); return { success: true, data: tasksArray, message: "Successfully fetched TimeCamp projects and tasks (non-archived only)" }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } async getTimeEntryById(entryId: string, date?: string) { try { let fromDate: string; let toDate: string; if (date) { // Use provided date fromDate = date; toDate = date; } else { // Use last 30 days const today = new Date(); const thirtyDaysAgo = new Date(today); thirtyDaysAgo.setDate(today.getDate() - 30); fromDate = thirtyDaysAgo.toISOString().split('T')[0]; toDate = today.toISOString().split('T')[0]; } // Fetch time entries for the specified date range const entriesResult = await this.getTimeEntries(fromDate, toDate, "me", "tags"); if (!entriesResult.success) { throw new Error(`Could not fetch time entries: ${entriesResult.error}`); } // Find the specific entry by ID const targetEntry = (entriesResult.data as any[]).find(entry => entry.id === parseInt(entryId)); if (!targetEntry) { throw new Error(`Time entry with ID ${entryId} not found in the specified date range`); } return { success: true, data: targetEntry, message: `Successfully found time entry with ID ${entryId}` }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } async deleteTimeEntry(entryId: string) { try { // Prepare request body as URL-encoded form data const requestBody = `id=${entryId}&service=timecamp-mcp`; // Make API request const response = await fetch(`${this.baseUrl}/entries`, { method: "DELETE", headers: { "Accept": "application/json", "Authorization": `Bearer ${this.bearerToken}`, "Content-Type": "application/x-www-form-urlencoded", }, body: requestBody, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`TimeCamp API request failed with status ${response.status}: ${errorText}`); } const result = await response.json(); return { success: true, data: result, message: `Successfully deleted time entry with ID: ${entryId}` }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } async updateTimeEntry(entryId: string, from?: string, to?: string, note?: string, task_id?: string) { try { // Prepare request body with required fields const requestBody: any = { id: parseInt(entryId), service: 'timecamp-mcp', }; let duration = 0; let timeUpdated = false; let finalStartTime = from; let finalEndTime = to; // Handle time updates if (from || to) { // Validate time format (HH:MM) for provided times const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/; if (from && !timeRegex.test(from)) { throw new Error("Start time format must be HH:MM (e.g., 15:28)"); } if (to && !timeRegex.test(to)) { throw new Error("End time format must be HH:MM (e.g., 15:28)"); } // If only one time is provided, fetch existing entry to get the other time if ((from && !to) || (!from && to)) { // Fetch the existing entry details (searches last 30 days if not found today) const entryResult = await this.getTimeEntryById(entryId); if (!entryResult.success) { throw new Error(`Could not fetch existing time entry: ${entryResult.error}`); } const existingEntry = entryResult.data; // Extract existing times (format: "HH:MM:SS") const existingStartTime = existingEntry.start_time?.substring(0, 5); // Get HH:MM part const existingEndTime = existingEntry.end_time?.substring(0, 5); // Get HH:MM part // Use existing times for missing values if (!from && to) { finalStartTime = existingStartTime; finalEndTime = to; } else if (from && !to) { finalStartTime = from; finalEndTime = existingEndTime; } if (!finalStartTime || !finalEndTime) { throw new Error("Could not determine existing start or end time from the current entry"); } } // Now we should have both start and end times if (finalStartTime && finalEndTime) { // Add seconds if not present (API expects HH:MM:SS) const startTimeFormatted = finalStartTime.includes(':') && finalStartTime.split(':').length === 2 ? finalStartTime + ':00' : finalStartTime; const endTimeFormatted = finalEndTime.includes(':') && finalEndTime.split(':').length === 2 ? finalEndTime + ':00' : finalEndTime; // Calculate duration (assuming same day) const startMinutes = parseInt(finalStartTime.split(':')[0]) * 60 + parseInt(finalStartTime.split(':')[1]); const endMinutes = parseInt(finalEndTime.split(':')[0]) * 60 + parseInt(finalEndTime.split(':')[1]); duration = (endMinutes - startMinutes) * 60; // Convert to seconds if (duration <= 0) { throw new Error("End time must be after start time"); } requestBody.start_time = startTimeFormatted; requestBody.end_time = endTimeFormatted; requestBody.duration = duration; timeUpdated = true; } } if (note !== undefined) { requestBody.note = note; } if (task_id) { requestBody.task_id = parseInt(task_id); } // Make API request const response = await fetch(`${this.baseUrl}/entries`, { method: "PUT", headers: this.getHeaders(), body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`TimeCamp API request failed with status ${response.status}: ${errorText}`); } const result = await response.json(); // Create appropriate success message let message = `Successfully updated time entry ID ${entryId}`; if (timeUpdated) { message += ` with time from ${finalStartTime} to ${finalEndTime} (${Math.floor(duration / 60)} minutes)`; } message += '.'; return { success: true, duration: timeUpdated ? Math.floor(duration / 60) : undefined, // duration in minutes if updated data: result, message: message }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } }

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/timecamp-org/timecamp-mcp-server'

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