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;
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate this is a non-readOnly, non-destructive operation. The description adds valuable behavioral context by explaining how Claude should parse natural language inputs and extract parameters, which goes beyond what annotations provide. It doesn't mention rate limits or authentication needs, but adds practical usage context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded with the core purpose statement. The natural language examples and extraction guidelines are useful but could be more concise. Each section adds value, though the parameter extraction list somewhat duplicates information already in the schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with no output schema but excellent schema coverage (100%) and comprehensive annotations, the description provides good contextual completeness. It explains the tool's purpose, shows practical usage examples, and clarifies parameter extraction from natural language. The main gap is the lack of information about what happens after logging (confirmation, error cases, etc.).

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema description coverage, the input schema already documents all parameters thoroughly. The description adds some semantic context by showing natural language mappings to parameters (e.g., '2h on security review' → task, duration) and explaining defaults, but doesn't significantly enhance the schema's parameter documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with a specific verb ('log') and resource ('completed time entry'), explaining it records work done. It distinguishes this from potential alternatives by specifying it's for logging completed time entries, though no sibling tools exist for comparison.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides implied usage through natural language examples showing when to use this tool (e.g., '2h on security review'), but lacks explicit guidance on when not to use it or alternatives. Since there are no sibling tools, the absence of comparative guidance is less critical.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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