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
TableJSON 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; }