Skip to main content
Glama

log_time

Record completed work sessions with task details, duration, and optional date/tags to maintain accurate time tracking records.

Instructions

Log a completed time entry. This tool records work you've done.

Natural language examples Claude should parse:

  • "2h on security review" → task: "security review", duration: "2h"

  • "Client meeting yesterday 90 minutes" → task: "Client meeting", duration: "90m", date: "yesterday"

  • "Just finished 1.5h on code review" → task: "code review", duration: "1.5h"

Claude should extract:

  • task: What was worked on

  • duration: How long (2h, 90m, 1.5h, etc.)

  • time: When (optional, defaults to now)

  • date: Which day (optional, defaults to today)

  • tags: Inferred or explicit tags

  • company: Which company (optional, uses default)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
companyNoCompany to log time for (optional, uses default if omitted)
dateNoDate of work (e.g., "today", "yesterday", "2025-10-17", omit for today)
durationYesDuration (e.g., "2h", "90m", "1.5h")
tagsNoTags to categorize work (e.g., ["development", "security"])
taskYesTask description (e.g., "Conduit MCP: Security review")
timeNoTime when work was done (e.g., "14:30", "2 hours ago", omit for now)

Implementation Reference

  • The core handler function for the 'log_time' tool. It processes LogTimeInput arguments: resolves company, sanitizes task, parses duration/date/time/tags, creates a TimeEntry, saves it to the company's markdown file via MarkdownManager, loads weekly summary, checks parse issues, and constructs a detailed markdown response with totals, commitment progress, and warnings.
    handler: withErrorHandler('logging time', async (args: LogTimeInput) => {
        // Resolve company (single-company mode auto-selects, multi-company requires explicit)
        const userInput = `${args.task || ''} ${args.duration || ''}`.trim();
        const company = getCompanyForOperation(args.company, userInput);
    
        // Sanitize and validate task description
        const sanitizedTask = sanitizeTaskDescription(args.task);
    
        // Parse inputs
        const duration = parseDuration(args.duration);
        const date = parseDate(args.date);
        const time = parseTime(args.time, date);
        const tags = args.tags || [];
    
        // Create time entry
        const entry: TimeEntry = {
            time: formatTime(time),
            task: sanitizedTask,
            duration: duration.hours,
            tags,
            date: formatDate(date)
        };
    
        // Save to markdown
        await markdownManager.addEntry(company, entry);
    
        // Load weekly summary for confirmation
        const { year, week } = getISOWeek(date);
        const summary = await markdownManager.getWeeklySummary(company, year, week);
    
        // Check for parse issues
        const parseIssues = markdownManager.getParseIssues();
    
        // Build response
        let response = `✓ Logged ${duration.formatted} for "${sanitizedTask}" at ${entry.time}`;
    
        if (args.date && args.date !== 'today') {
            response += ` on ${entry.date}`;
        }
    
        if (tags.length > 0) {
            response += ` [${tags.map(t => '#' + t).join(' ')}]`;
        }
    
        response += '\n\n';
    
        if (summary) {
            response += `**Week ${week} Status:**\n`;
            response += `• Total: ${summary.totalHours.toFixed(1)}h`;
    
            const config = await markdownManager.loadConfig(company);
            if (config.commitments.total) {
                const limit = config.commitments.total.limit;
                const stats = summaryCalculator.getCommitmentStats(summary.totalHours, limit);
                response += ` / ${limit}h (${stats.percentage}%)`;
    
                if (stats.status === 'over') {
                    response += ` ⚠️ OVER LIMIT`;
                } else if (stats.status === 'approaching') {
                    response += ` ⚠️ Close to limit`;
                }
            }
    
            response += '\n';
    
            // Show commitment breakdown
            for (const [commitment, hours] of Object.entries(summary.byCommitment)) {
                const limit = config.commitments[commitment]?.limit;
                if (limit) {
                    const stats = summaryCalculator.getCommitmentStats(hours, limit);
                    const warning = stats.status !== 'within' ? ` ${stats.indicator}` : '';
                    response += `• ${capitalizeName(commitment)}: ${hours.toFixed(1)}h / ${limit}h (${stats.percentage}%)${warning}\n`;
                }
            }
        }
    
        // Add parse warnings if any
        if (parseIssues.warnings.length > 0) {
            response += '\n\n⚠️ **Parse Warnings:**\n';
            for (const warning of parseIssues.warnings) {
                response += `• ${warning}\n`;
            }
            if (parseIssues.unparsedLines.length > 0 && parseIssues.unparsedLines.length <= 3) {
                response += '\nUnparsed lines:\n';
                for (const { lineNumber, content } of parseIssues.unparsedLines) {
                    response += `  Line ${lineNumber}: ${content}\n`;
                }
            }
        }
    
        return createTextResponse(response);
    })
  • JSON Schema definition for LogTimeInput used by the log_time tool, specifying properties for task, duration (required), time, date, tags, and optional company.
    inputSchema: {
        type: 'object',
        properties: {
            task: {
                type: 'string',
                description: 'Task description (e.g., "Conduit MCP: Security review")'
            },
            duration: {
                type: 'string',
                description: 'Duration (e.g., "2h", "90m", "1.5h")'
            },
            time: {
                type: 'string',
                description: 'Time when work was done (e.g., "14:30", "2 hours ago", omit for now)'
            },
            date: {
                type: 'string',
                description: 'Date of work (e.g., "today", "yesterday", "2025-10-17", omit for today)'
            },
            tags: {
                type: 'array',
                items: { type: 'string' },
                description: 'Tags to categorize work (e.g., ["development", "security"])'
            },
            company: {
                type: 'string',
                description: 'Company to log time for (optional, uses default if omitted)'
            }
        },
        required: ['task', 'duration']
    },
  • Registration of the 'log_time' tool using registerTool from ./registry.js. Includes name, detailed description with parsing examples, inputSchema, annotations, and handler.
    registerTool({
        name: 'log_time',
        description: `Log a completed time entry. This tool records work you've done.
    
    Natural language examples Claude should parse:
    - Single company mode: "2h on security review" → task: "security review", duration: "2h"
    - Multi company mode: "hm 2h on security review" → company: "HeliMods", duration: "2h", task: "security review"
    - Multi company mode: "2h debugging for stellantis" → duration: "2h", task: "debugging", company: "Stellantis"
    - "Client meeting yesterday 90 minutes" → task: "Client meeting", duration: "90m", date: "yesterday"
    
    Claude should extract:
    - task: What was worked on
    - duration: How long (2h, 90m, 1.5h, etc.)
    - time: When (optional, defaults to now)
    - date: Which day (optional, defaults to today)
    - tags: Inferred or explicit tags
    - company: CRITICAL - In multi-company mode, MUST extract from user input using:
      * Prefix pattern: "company/abbrev [duration] [task]" (e.g., "hm 2h debugging", "stellantis 1h meeting")
      * Suffix pattern: "[duration] [task] for company/abbrev" (e.g., "2h debugging for hm", "1h meeting for stellantis")
      * Full company names work (case-insensitive): "HeliMods", "Stellantis", etc.
      * Abbreviations work (case-insensitive): "HM", "STLA", etc.
      * In single-company mode, company is automatic (ignore any company in input)`,
        inputSchema: {
            type: 'object',
            properties: {
                task: {
                    type: 'string',
                    description: 'Task description (e.g., "Conduit MCP: Security review")'
                },
                duration: {
                    type: 'string',
                    description: 'Duration (e.g., "2h", "90m", "1.5h")'
                },
                time: {
                    type: 'string',
                    description: 'Time when work was done (e.g., "14:30", "2 hours ago", omit for now)'
                },
                date: {
                    type: 'string',
                    description: 'Date of work (e.g., "today", "yesterday", "2025-10-17", omit for today)'
                },
                tags: {
                    type: 'array',
                    items: { type: 'string' },
                    description: 'Tags to categorize work (e.g., ["development", "security"])'
                },
                company: {
                    type: 'string',
                    description: 'Company to log time for (optional, uses default if omitted)'
                }
            },
            required: ['task', 'duration']
        },
        annotations: {
            title: 'Log Time',
            readOnlyHint: false,
            destructiveHint: false,
            idempotentHint: false,
            openWorldHint: false
        },
        handler: withErrorHandler('logging time', async (args: LogTimeInput) => {
            // Resolve company (single-company mode auto-selects, multi-company requires explicit)
            const userInput = `${args.task || ''} ${args.duration || ''}`.trim();
            const company = getCompanyForOperation(args.company, userInput);
    
            // Sanitize and validate task description
            const sanitizedTask = sanitizeTaskDescription(args.task);
    
            // Parse inputs
            const duration = parseDuration(args.duration);
            const date = parseDate(args.date);
            const time = parseTime(args.time, date);
            const tags = args.tags || [];
    
            // Create time entry
            const entry: TimeEntry = {
                time: formatTime(time),
                task: sanitizedTask,
                duration: duration.hours,
                tags,
                date: formatDate(date)
            };
    
            // Save to markdown
            await markdownManager.addEntry(company, entry);
    
            // Load weekly summary for confirmation
            const { year, week } = getISOWeek(date);
            const summary = await markdownManager.getWeeklySummary(company, year, week);
    
            // Check for parse issues
            const parseIssues = markdownManager.getParseIssues();
    
            // Build response
            let response = `✓ Logged ${duration.formatted} for "${sanitizedTask}" at ${entry.time}`;
    
            if (args.date && args.date !== 'today') {
                response += ` on ${entry.date}`;
            }
    
            if (tags.length > 0) {
                response += ` [${tags.map(t => '#' + t).join(' ')}]`;
            }
    
            response += '\n\n';
    
            if (summary) {
                response += `**Week ${week} Status:**\n`;
                response += `• Total: ${summary.totalHours.toFixed(1)}h`;
    
                const config = await markdownManager.loadConfig(company);
                if (config.commitments.total) {
                    const limit = config.commitments.total.limit;
                    const stats = summaryCalculator.getCommitmentStats(summary.totalHours, limit);
                    response += ` / ${limit}h (${stats.percentage}%)`;
    
                    if (stats.status === 'over') {
                        response += ` ⚠️ OVER LIMIT`;
                    } else if (stats.status === 'approaching') {
                        response += ` ⚠️ Close to limit`;
                    }
                }
    
                response += '\n';
    
                // Show commitment breakdown
                for (const [commitment, hours] of Object.entries(summary.byCommitment)) {
                    const limit = config.commitments[commitment]?.limit;
                    if (limit) {
                        const stats = summaryCalculator.getCommitmentStats(hours, limit);
                        const warning = stats.status !== 'within' ? ` ${stats.indicator}` : '';
                        response += `• ${capitalizeName(commitment)}: ${hours.toFixed(1)}h / ${limit}h (${stats.percentage}%)${warning}\n`;
                    }
                }
            }
    
            // Add parse warnings if any
            if (parseIssues.warnings.length > 0) {
                response += '\n\n⚠️ **Parse Warnings:**\n';
                for (const warning of parseIssues.warnings) {
                    response += `• ${warning}\n`;
                }
                if (parseIssues.unparsedLines.length > 0 && parseIssues.unparsedLines.length <= 3) {
                    response += '\nUnparsed lines:\n';
                    for (const { lineNumber, content } of parseIssues.unparsedLines) {
                        response += `  Line ${lineNumber}: ${content}\n`;
                    }
                }
            }
    
            return createTextResponse(response);
        })
    });
  • TypeScript interface definition for LogTimeInput, matching the tool's input schema and used in the handler signature.
    export interface LogTimeInput {
        task: string;
        duration: string;       // "2h", "90m", "1.5h", etc.
        time?: string;          // "14:30", "2 hours ago", omit for now
        date?: string;          // "today", "yesterday", "2025-10-17"
        tags?: string[];
        company?: string;
    }
Install Server

Other Tools

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/markwharton/time-tracking-mcp'

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