log_time
Log time entries in Harvest using natural language commands to track work hours, projects, and tasks with automatic date parsing.
Instructions
Log time entry using natural language
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| text | Yes | Natural language time entry (e.g. "2 hours on Project X doing development work yesterday") |
Implementation Reference
- src/index.ts:338-380 (handler)Handler for the 'log_time' tool. Parses natural language input to extract date, hours, project, and task, then creates a time entry in Harvest via API.
case 'log_time': { const { text } = request.params.arguments as { text: string }; try { // Parse time entry details const { spent_date, hours, isLeave, leaveType } = await this.parseTimeEntry(text); // Find matching project const project_id = await this.findProject(text, isLeave, leaveType); // Find matching task const task_id = await this.findTask(project_id, text, isLeave, leaveType); // Create time entry const response = await this.axiosInstance.post('/time_entries', { project_id, task_id, spent_date, hours, notes: text, }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { if (error instanceof McpError) { throw error; } if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InternalError, `Harvest API error: ${error.response?.data?.message ?? error.message}` ); } throw error; } } - src/index.ts:267-279 (registration)Registration of the 'log_time' tool in the ListTools response, including description and input schema definition.
name: 'log_time', description: 'Log time entry using natural language', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'Natural language time entry (e.g. "2 hours on Project X doing development work yesterday")', }, }, required: ['text'], }, }, - src/index.ts:165-208 (helper)Helper method that parses the natural language text input into structured time entry data: date, hours, and leave detection.
private async parseTimeEntry(text: string) { const lowercaseText = text.toLowerCase(); const now = new Date(new Date().toLocaleString('en-US', { timeZone: TIMEZONE })); // Check if this is a leave request const leaveCheck = this.isLeaveRequest(text); if (leaveCheck.isLeave && leaveCheck.type) { // For leave requests, use the full work day return { spent_date: now.toISOString().split('T')[0], hours: STANDARD_WORK_DAY_HOURS, isLeave: true, leaveType: leaveCheck.type }; } // For regular time entries let date: Date; if (lowercaseText.includes('today')) { date = now; } else { const parsed = chrono.parseDate(text); if (!parsed) { throw new McpError(ErrorCode.InvalidParams, 'Could not parse date from input'); } date = parsed; } // Extract hours/minutes const durationMatch = text.match(/(\d+)\s*(hour|hr|h|minute|min|m)s?/i); if (!durationMatch) { throw new McpError(ErrorCode.InvalidParams, 'Could not parse duration from input'); } const amount = parseInt(durationMatch[1]); const unit = durationMatch[2].toLowerCase(); const hours = unit.startsWith('h') ? amount : amount / 60; return { spent_date: date.toISOString().split('T')[0], hours, isLeave: false }; } - src/index.ts:210-234 (helper)Helper method to find the appropriate project ID by matching the input text or using predefined leave project.
private async findProject(text: string, isLeave: boolean = false, leaveType?: keyof typeof LEAVE_PATTERNS): Promise<number> { const response = await this.axiosInstance.get('/projects'); const projects = response.data.projects; if (isLeave && leaveType) { // For leave requests, look for the specific leave project const leaveProject = projects.find((p: { name: string; id: number }) => p.name === LEAVE_PATTERNS[leaveType].project ); if (leaveProject) { return leaveProject.id; } } // For regular entries or if leave project not found const projectMatch = projects.find((p: { name: string; id: number }) => text.toLowerCase().includes(p.name.toLowerCase()) ); if (!projectMatch) { throw new McpError(ErrorCode.InvalidParams, 'Could not find matching project'); } return projectMatch.id; } - src/index.ts:236-261 (helper)Helper method to find the task ID for the given project by matching text or leave task.
private async findTask(projectId: number, text: string, isLeave: boolean = false, leaveType?: keyof typeof LEAVE_PATTERNS): Promise<number> { const response = await this.axiosInstance.get(`/projects/${projectId}/task_assignments`); const tasks = response.data.task_assignments; if (isLeave && leaveType) { // For leave requests, look for the specific leave task const leaveTask = tasks.find((t: { task: { name: string; id: number } }) => t.task.name === LEAVE_PATTERNS[leaveType].task ); if (leaveTask) { return leaveTask.task.id; } } // For regular entries or if leave task not found const taskMatch = tasks.find((t: { task: { name: string; id: number } }) => text.toLowerCase().includes(t.task.name.toLowerCase()) ); if (!taskMatch) { // Default to first task if no match found return tasks[0].task.id; } return taskMatch.task.id; }