UNPKG

@asanstefanski/everhour-mcp-server

Version:

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

479 lines 19.2 kB
import { z } from 'zod'; // Zod schemas for input validation const ListTimeRecordsSchema = z.object({ page: z.number().optional(), limit: z.number().optional(), project: z.string().optional(), assignee: z.number().optional(), from: z.string().optional().refine((date) => { if (!date) return true; return /^\d{4}-\d{2}-\d{2}$/.test(date); }, 'Date must be in YYYY-MM-DD format'), to: z.string().optional().refine((date) => { if (!date) return true; return /^\d{4}-\d{2}-\d{2}$/.test(date); }, 'Date must be in YYYY-MM-DD format'), }); const GetTimeRecordSchema = z.object({ id: z.number(), }); const CreateTimeRecordSchema = z.object({ time: z.union([ z.number().positive('Time must be positive'), z.string().min(1, 'Time is required') ]), date: z.string().refine((date) => { return /^\d{4}-\d{2}-\d{2}$/.test(date); }, 'Date must be in YYYY-MM-DD format'), task: z.string().optional(), project: z.string().optional(), comment: z.string().optional(), }); const UpdateTimeRecordSchema = z.object({ id: z.number(), time: z.union([ z.number().positive('Time must be positive'), z.string().min(1, 'Time is required') ]).optional(), date: z.string().optional().refine((date) => { if (!date) return true; return /^\d{4}-\d{2}-\d{2}$/.test(date); }, 'Date must be in YYYY-MM-DD format'), task: z.string().optional(), project: z.string().optional(), comment: z.string().optional(), }); const DeleteTimeRecordSchema = z.object({ id: z.number(), }); export const timeRecordTools = { everhour_list_time_records: { name: 'everhour_list_time_records', description: 'List time records from Everhour. Supports filtering by project, assignee, and date range.', readonly: true, operationType: 'read', affectedResources: ['time'], inputSchema: { type: 'object', properties: { page: { type: 'number', description: 'Page number for pagination (default: 1)', }, limit: { type: 'number', description: 'Number of time records per page (default: 100)', }, project: { type: 'string', description: 'Filter time records by project ID', }, assignee: { type: 'number', description: 'Filter time records by assignee user ID', }, from: { type: 'string', description: 'Start date for filtering (YYYY-MM-DD format)', }, to: { type: 'string', description: 'End date for filtering (YYYY-MM-DD format)', }, }, }, handler: async (client, args) => { const params = ListTimeRecordsSchema.parse(args); try { const timeRecords = await client.getTimeRecords(params); return { content: [ { type: 'text', text: JSON.stringify({ timeRecords: timeRecords.map(record => ({ id: record.id, comment: record.comment, time: record.time, timeFormatted: client.formatTime(record.time), date: record.date, task: record.task, project: record.project, user: record.user, createdAt: record.createdAt, updatedAt: record.updatedAt, })), total: timeRecords.length, totalTime: timeRecords.reduce((sum, record) => sum + record.time, 0), totalTimeFormatted: client.formatTime(timeRecords.reduce((sum, record) => sum + record.time, 0)), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error listing time records: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }, everhour_get_time_record: { name: 'everhour_get_time_record', description: 'Get details of a specific time record by ID.', readonly: true, operationType: 'read', affectedResources: ['time'], inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Time record ID', }, }, required: ['id'], }, handler: async (client, args) => { const { id } = GetTimeRecordSchema.parse(args); try { const timeRecord = await client.getTimeRecord(id); return { content: [ { type: 'text', text: JSON.stringify({ timeRecord: { id: timeRecord.id, comment: timeRecord.comment, time: timeRecord.time, timeFormatted: client.formatTime(timeRecord.time), date: timeRecord.date, task: timeRecord.task, project: timeRecord.project, user: timeRecord.user, createdAt: timeRecord.createdAt, updatedAt: timeRecord.updatedAt, }, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error getting time record: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }, everhour_create_time_record: { name: 'everhour_create_time_record', description: 'Create a new time record in Everhour. Time can be specified in seconds or human-readable format (e.g., "1h 30m", "90m", "5400s").', readonly: false, operationType: 'write', affectedResources: ['time'], inputSchema: { type: 'object', properties: { time: { type: ['number', 'string'], description: 'Time in seconds (number) or human-readable format (string like "1h 30m", "90m", "5400s")', }, date: { type: 'string', description: 'Date in YYYY-MM-DD format', }, task: { type: 'string', description: 'Task ID to associate with the time record', }, project: { type: 'string', description: 'Project ID to associate with the time record', }, comment: { type: 'string', description: 'Optional comment for the time record', }, }, required: ['time', 'date'], }, handler: async (client, args) => { const params = CreateTimeRecordSchema.parse(args); try { // Convert time to seconds if it's a string const timeInSeconds = typeof params.time === 'string' ? client.parseTimeToSeconds(params.time) : params.time; const createParams = { ...params, time: timeInSeconds, }; const timeRecord = await client.createTimeRecord(createParams); return { content: [ { type: 'text', text: JSON.stringify({ success: true, timeRecord: { id: timeRecord.id, comment: timeRecord.comment, time: timeRecord.time, timeFormatted: client.formatTime(timeRecord.time), date: timeRecord.date, task: timeRecord.task, project: timeRecord.project, user: timeRecord.user, createdAt: timeRecord.createdAt, }, message: `Time record created successfully: ${client.formatTime(timeRecord.time)} on ${timeRecord.date}`, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error creating time record: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }, everhour_update_time_record: { name: 'everhour_update_time_record', description: 'Update an existing time record in Everhour. Time can be specified in seconds or human-readable format.', readonly: false, operationType: 'write', affectedResources: ['time'], inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Time record ID', }, time: { type: ['number', 'string'], description: 'Time in seconds (number) or human-readable format (string like "1h 30m", "90m", "5400s")', }, date: { type: 'string', description: 'Date in YYYY-MM-DD format', }, task: { type: 'string', description: 'Task ID to associate with the time record', }, project: { type: 'string', description: 'Project ID to associate with the time record', }, comment: { type: 'string', description: 'Comment for the time record', }, }, required: ['id'], }, handler: async (client, args) => { const { id, time, ...otherParams } = UpdateTimeRecordSchema.parse(args); try { // Convert time to seconds if it's provided as a string const timeInSeconds = time !== undefined && typeof time === 'string' ? client.parseTimeToSeconds(time) : time; const updateParams = { ...otherParams, time: timeInSeconds, }; const timeRecord = await client.updateTimeRecord(id, updateParams); return { content: [ { type: 'text', text: JSON.stringify({ success: true, timeRecord: { id: timeRecord.id, comment: timeRecord.comment, time: timeRecord.time, timeFormatted: client.formatTime(timeRecord.time), date: timeRecord.date, task: timeRecord.task, project: timeRecord.project, user: timeRecord.user, updatedAt: timeRecord.updatedAt, }, message: `Time record updated successfully: ${client.formatTime(timeRecord.time)} on ${timeRecord.date}`, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error updating time record: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }, everhour_delete_time_record: { name: 'everhour_delete_time_record', description: 'Delete a time record from Everhour. This action cannot be undone.', readonly: false, operationType: 'delete', affectedResources: ['time'], inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Time record ID', }, }, required: ['id'], }, handler: async (client, args) => { const { id } = DeleteTimeRecordSchema.parse(args); try { await client.deleteTimeRecord(id); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: `Time record with ID "${id}" deleted successfully`, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error deleting time record: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }, everhour_get_user_time: { name: 'everhour_get_user_time', description: 'Get time records for a specific user with date range and pagination support.', readonly: true, operationType: 'read', affectedResources: ['time'], inputSchema: { type: 'object', properties: { userId: { type: 'number', description: 'User ID to get time records for', }, from: { type: 'string', description: 'Start date for time records (YYYY-MM-DD format, e.g., "2024-01-01")', }, to: { type: 'string', description: 'End date for time records (YYYY-MM-DD format, e.g., "2024-01-31")', }, limit: { type: 'number', description: 'Maximum number of results per page (default: 100)', }, page: { type: 'number', description: 'Page number for pagination (default: 1)', }, }, required: ['userId'], }, handler: async (client, args) => { const { userId, from, to, limit, page } = args; try { const timeRecords = await client.getUserTime(userId, { from, to, limit, page, }); return { content: [ { type: 'text', text: JSON.stringify({ timeRecords: timeRecords.map(record => ({ id: record.id, comment: record.comment, time: record.time, timeFormatted: client.formatTime(record.time), date: record.date, task: record.task, project: record.project, user: record.user, createdAt: record.createdAt, updatedAt: record.updatedAt, })), total: timeRecords.length, totalTime: timeRecords.reduce((sum, record) => sum + record.time, 0), totalTimeFormatted: client.formatTime(timeRecords.reduce((sum, record) => sum + record.time, 0)), dateRange: from && to ? `${from} to ${to}` : 'All time', pagination: { page: page || 1, limit: limit || 100, }, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error getting user time records: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }, }; //# sourceMappingURL=time-records.js.map