/**
* MCP Tool Definitions for Clockify
* All tools that can be invoked by the LLM
*/
import { z } from 'zod';
import { ClockifyClient } from './clockify-client.js';
// ═══════════════════════════════════════════════════════════
// HELPER FUNCTIONS
// ═══════════════════════════════════════════════════════════
function formatDuration(ms: number): string {
const hours = Math.floor(ms / 3600000);
const minutes = Math.floor((ms % 3600000) / 60000);
return `${hours}h ${minutes}m`;
}
function formatMoney(amount: number, currency = 'USD'): string {
// Clockify stores amounts in cents
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount / 100);
}
// Parse human-readable duration like "1h30m", "2h", "45m" to ISO 8601 end time
function parseDurationToEndTime(start: Date, duration: string): Date {
// Check for negative values
if (duration.includes('-')) {
throw new Error(`Duration cannot be negative: "${duration}". Use positive values like "1h30m".`);
}
const hourMatch = duration.match(/(\d+)\s*h/i);
const minMatch = duration.match(/(\d+)\s*m/i);
const hours = hourMatch ? parseInt(hourMatch[1], 10) : 0;
const minutes = minMatch ? parseInt(minMatch[1], 10) : 0;
if (hours === 0 && minutes === 0) {
throw new Error(`Duration must be greater than 0. Use format like "1h30m", "2h", or "45m".`);
}
const totalMs = (hours * 60 + minutes) * 60 * 1000;
return new Date(start.getTime() + totalMs);
}
// Validate time entry start date is reasonable
function validateStartTime(start: string): void {
const startDate = new Date(start);
const now = new Date();
if (isNaN(startDate.getTime())) {
throw new Error(`Invalid start time: "${start}". Use ISO 8601 format (e.g., 2025-01-15T09:00:00Z)`);
}
const diffMs = startDate.getTime() - now.getTime();
const diffDays = diffMs / (1000 * 60 * 60 * 24);
// More than 1 day in future
if (diffDays > 1) {
throw new Error(`Start time "${start}" is more than 1 day in the future. Did you mean a different date?`);
}
// More than 365 days in past
if (diffDays < -365) {
throw new Error(`Start time "${start}" is more than 1 year in the past. Did you mean a different year?`);
}
}
// Parse ISO 8601 duration (PT1H30M) to human-readable format
function parseIsoDuration(isoDuration: string | undefined): string {
if (!isoDuration) return '0h 0m';
const hourMatch = isoDuration.match(/(\d+)H/);
const minMatch = isoDuration.match(/(\d+)M/);
const secMatch = isoDuration.match(/(\d+)S/);
const hours = hourMatch ? parseInt(hourMatch[1], 10) : 0;
const minutes = minMatch ? parseInt(minMatch[1], 10) : 0;
const seconds = secMatch ? parseInt(secMatch[1], 10) : 0;
if (hours > 0) {
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
}
if (minutes > 0) {
return `${minutes}m`;
}
return `${seconds}s`;
}
// Get date range for relative periods
function getDateRange(period: string): { start: string; end: string } {
const now = new Date();
let start: Date;
let end: Date = new Date(now);
switch (period.toLowerCase()) {
case 'today':
start = new Date(now);
start.setHours(0, 0, 0, 0);
end.setHours(23, 59, 59, 999);
break;
case 'yesterday':
start = new Date(now);
start.setDate(start.getDate() - 1);
start.setHours(0, 0, 0, 0);
end = new Date(start);
end.setHours(23, 59, 59, 999);
break;
case 'this_week':
start = new Date(now);
start.setDate(start.getDate() - start.getDay() + 1); // Monday
start.setHours(0, 0, 0, 0);
end.setHours(23, 59, 59, 999);
break;
case 'last_week':
start = new Date(now);
start.setDate(start.getDate() - start.getDay() - 6); // Last Monday
start.setHours(0, 0, 0, 0);
end = new Date(start);
end.setDate(end.getDate() + 6); // Last Sunday
end.setHours(23, 59, 59, 999);
break;
case 'this_month':
start = new Date(now.getFullYear(), now.getMonth(), 1);
end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
break;
case 'last_month':
start = new Date(now.getFullYear(), now.getMonth() - 1, 1);
end = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
break;
default:
throw new Error(`Unknown period: "${period}". Use: today, yesterday, this_week, last_week, this_month, last_month`);
}
return {
start: start.toISOString(),
end: end.toISOString(),
};
}
// ═══════════════════════════════════════════════════════════
// TOOL SCHEMAS
// ═══════════════════════════════════════════════════════════
export const schemas = {
// Workspace tools
getWorkspaces: {},
getCurrentUser: {},
getWorkspaceUsers: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
},
// Project tools
getProjects: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
archived: z.boolean().optional().describe('Filter by archived status'),
},
createProject: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
name: z.string().describe('Project name'),
clientId: z.string().optional().describe('Client ID to associate'),
color: z.string().optional().describe('Project color (hex code like #FF5733)'),
billable: z.boolean().optional().describe('Whether project is billable'),
},
getProjectTasks: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
projectId: z.string().describe('The project ID (use get_projects to find IDs)'),
},
createTask: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
projectId: z.string().describe('The project ID (use get_projects to find IDs)'),
name: z.string().describe('Task name'),
assigneeIds: z.array(z.string()).optional().describe('User IDs to assign'),
},
// Time entry tools
getTimeEntries: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
userId: z.string().optional().describe('User ID (defaults to current user)'),
start: z.string().optional().describe('Start date (ISO 8601, e.g., 2024-01-01T00:00:00Z)'),
end: z.string().optional().describe('End date (ISO 8601)'),
period: z.string().optional().describe('Relative period: today, yesterday, this_week, last_week, this_month, last_month'),
projectId: z.string().optional().describe('Filter by project ID'),
},
createTimeEntry: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
start: z.string().describe('Start time (ISO 8601). Use current time to start a timer.'),
end: z.string().optional().describe('End time (ISO 8601). Omit to start a running timer.'),
description: z.string().optional().describe('Description of work done'),
projectId: z.string().optional().describe('Project ID'),
taskId: z.string().optional().describe('Task ID'),
billable: z.boolean().optional().describe('Whether entry is billable'),
},
stopTimer: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
userId: z.string().optional().describe('User ID (defaults to current user)'),
},
deleteTimeEntry: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
timeEntryId: z.string().describe('The time entry ID to delete'),
},
updateTimeEntry: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
timeEntryId: z.string().describe('The time entry ID to update'),
start: z.string().optional().describe('New start time (ISO 8601)'),
end: z.string().optional().describe('New end time (ISO 8601)'),
description: z.string().optional().describe('New description'),
projectId: z.string().optional().describe('New project ID'),
billable: z.boolean().optional().describe('New billable status'),
},
// Reporting tools
getSummaryReport: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
dateRangeStart: z.string().describe('Report start date (ISO 8601)'),
dateRangeEnd: z.string().describe('Report end date (ISO 8601)'),
groupBy: z.array(z.enum(['PROJECT', 'USER', 'CLIENT', 'TAG', 'TASK']))
.optional()
.describe('How to group results (default: PROJECT, USER)'),
},
getDetailedReport: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
dateRangeStart: z.string().describe('Report start date (ISO 8601)'),
dateRangeEnd: z.string().describe('Report end date (ISO 8601)'),
userIds: z.array(z.string()).optional().describe('Filter by user IDs'),
projectIds: z.array(z.string()).optional().describe('Filter by project IDs'),
},
// Tag & Client tools
getTags: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
},
createTag: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
name: z.string().describe('Tag name'),
},
getClients: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to active workspace)'),
},
// ═══════════════════════════════════════════════════════════
// CONVENIENCE TOOLS (simpler interfaces, auto-detect user/workspace)
// ═══════════════════════════════════════════════════════════
getRunningTimer: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to user\'s active workspace)'),
},
startTimer: {
description: z.string().optional().describe('What you are working on'),
projectId: z.string().optional().describe('Project ID'),
workspaceId: z.string().optional().describe('Workspace ID (defaults to user\'s active workspace)'),
billable: z.boolean().optional().describe('Whether this is billable'),
},
stopCurrentTimer: {
workspaceId: z.string().optional().describe('Workspace ID (defaults to user\'s active workspace)'),
end: z.string().optional().describe('End time override (ISO 8601, defaults to now). Use if you forgot to stop earlier.'),
},
logTime: {
description: z.string().optional().describe('What you worked on'),
duration: z.string().describe('Duration in human format (e.g., "1h30m", "2h", "45m")'),
projectId: z.string().optional().describe('Project ID'),
workspaceId: z.string().optional().describe('Workspace ID (defaults to user\'s active workspace)'),
billable: z.boolean().optional().describe('Whether this is billable'),
date: z.string().optional().describe('Date for the entry (ISO 8601, defaults to today)'),
},
};
// ═══════════════════════════════════════════════════════════
// TOOL HANDLERS
// ═══════════════════════════════════════════════════════════
export function createToolHandlers(client: ClockifyClient) {
return {
// ─────────────────────────────────────────────────────────
// WORKSPACE TOOLS
// ─────────────────────────────────────────────────────────
async getWorkspaces() {
const workspaces = await client.getWorkspaces();
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
workspaces.map((w) => ({
id: w.id,
name: w.name,
hourlyRate: w.hourlyRate,
})),
null,
2
),
},
],
};
},
async getCurrentUser() {
const user = await client.getCurrentUser();
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
id: user.id,
name: user.name,
email: user.email,
activeWorkspace: user.activeWorkspace,
},
null,
2
),
},
],
};
},
async getWorkspaceUsers({ workspaceId }: { workspaceId?: string }) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const users = await client.getWorkspaceUsers(wsId);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
users.map((u) => ({ id: u.id, name: u.name, email: u.email, status: u.status })),
null,
2
),
},
],
};
},
// ─────────────────────────────────────────────────────────
// PROJECT TOOLS
// ─────────────────────────────────────────────────────────
async getProjects({ workspaceId, archived }: { workspaceId?: string; archived?: boolean }) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const projects = await client.getProjects(wsId, { archived });
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
projects.map((p) => ({
id: p.id,
name: p.name,
clientName: p.clientName,
billable: p.billable,
archived: p.archived,
color: p.color,
})),
null,
2
),
},
],
};
},
async createProject({
workspaceId,
name,
clientId,
color,
billable,
}: {
workspaceId?: string;
name: string;
clientId?: string;
color?: string;
billable?: boolean;
}) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const project = await client.createProject(wsId, { name, clientId, color, billable });
return {
content: [
{
type: 'text' as const,
text: `Created project "${project.name}" (ID: ${project.id})`,
},
],
};
},
async getProjectTasks({ workspaceId, projectId }: { workspaceId?: string; projectId: string }) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const tasks = await client.getProjectTasks(wsId, projectId);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
tasks.map((t) => ({ id: t.id, name: t.name, status: t.status })),
null,
2
),
},
],
};
},
async createTask({
workspaceId,
projectId,
name,
assigneeIds,
}: {
workspaceId?: string;
projectId: string;
name: string;
assigneeIds?: string[];
}) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const task = await client.createTask(wsId, projectId, { name, assigneeIds });
return {
content: [{ type: 'text' as const, text: `Created task "${task.name}" (ID: ${task.id})` }],
};
},
// ─────────────────────────────────────────────────────────
// TIME ENTRY TOOLS
// ─────────────────────────────────────────────────────────
async getTimeEntries({
workspaceId,
userId,
start,
end,
period,
projectId,
}: {
workspaceId?: string;
userId?: string;
start?: string;
end?: string;
period?: string;
projectId?: string;
}) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const uid = userId || (await client.getCachedUser()).id;
// Handle relative period
let startDate = start;
let endDate = end;
if (period) {
const range = getDateRange(period);
startDate = range.start;
endDate = range.end;
}
const entries = await client.getTimeEntries(wsId, uid, {
start: startDate,
end: endDate,
project: projectId,
});
// Calculate total duration
let totalMs = 0;
const formattedEntries = entries.map((e) => {
const duration = parseIsoDuration(e.timeInterval.duration);
// Parse duration back to ms for total
const hourMatch = e.timeInterval.duration?.match(/(\d+)H/);
const minMatch = e.timeInterval.duration?.match(/(\d+)M/);
const hours = hourMatch ? parseInt(hourMatch[1], 10) : 0;
const minutes = minMatch ? parseInt(minMatch[1], 10) : 0;
totalMs += (hours * 60 + minutes) * 60 * 1000;
return {
id: e.id,
description: e.description || '(no description)',
project: e.projectId,
start: e.timeInterval.start,
end: e.timeInterval.end,
duration,
billable: e.billable,
};
});
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
total: formatDuration(totalMs),
count: entries.length,
entries: formattedEntries,
},
null,
2
),
},
],
};
},
async createTimeEntry({
workspaceId,
start,
end,
description,
projectId,
taskId,
billable,
}: {
workspaceId?: string;
start: string;
end?: string;
description?: string;
projectId?: string;
taskId?: string;
billable?: boolean;
}) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
// Validate start time to catch typos early
validateStartTime(start);
if (end) validateStartTime(end);
const entry = await client.createTimeEntry(wsId, {
start,
end,
description,
projectId,
taskId,
billable,
});
const msg = end
? `Created time entry: "${entry.description || 'No description'}" (${entry.timeInterval.duration})`
: `Started timer: "${entry.description || 'No description'}"`;
return { content: [{ type: 'text' as const, text: msg }] };
},
async stopTimer({ workspaceId, userId }: { workspaceId?: string; userId?: string }) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const uid = userId || (await client.getCachedUser()).id;
try {
const entry = await client.stopTimer(wsId, uid);
return {
content: [
{
type: 'text' as const,
text: `Stopped timer: "${entry.description || 'No description'}" (${parseIsoDuration(entry.timeInterval.duration)})`,
},
],
};
} catch (error) {
if (error instanceof Error && error.message.includes('No timer')) {
return { content: [{ type: 'text' as const, text: 'No timer currently running.' }] };
}
throw error;
}
},
async deleteTimeEntry({ workspaceId, timeEntryId }: { workspaceId?: string; timeEntryId: string }) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
try {
await client.deleteTimeEntry(wsId, timeEntryId);
return { content: [{ type: 'text' as const, text: `Deleted time entry ${timeEntryId}` }] };
} catch (error) {
if (error instanceof Error && (error.message.includes('404') || error.message.includes('not found'))) {
return { content: [{ type: 'text' as const, text: `Time entry ${timeEntryId} not found or already deleted.` }] };
}
throw error;
}
},
async updateTimeEntry({
workspaceId,
timeEntryId,
start,
end,
description,
projectId,
billable,
}: {
workspaceId?: string;
timeEntryId: string;
start?: string;
end?: string;
description?: string;
projectId?: string;
billable?: boolean;
}) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
try {
const entry = await client.updateTimeEntry(wsId, timeEntryId, {
start,
end,
description,
projectId,
billable,
});
return {
content: [{ type: 'text' as const, text: `Updated time entry: ${entry.id}` }],
};
} catch (error) {
if (error instanceof Error && (error.message.includes('404') || error.message.includes('not found'))) {
return { content: [{ type: 'text' as const, text: `Time entry ${timeEntryId} not found.` }] };
}
throw error;
}
},
// ─────────────────────────────────────────────────────────
// REPORTING TOOLS
// ─────────────────────────────────────────────────────────
async getSummaryReport({
workspaceId,
dateRangeStart,
dateRangeEnd,
groupBy,
}: {
workspaceId?: string;
dateRangeStart: string;
dateRangeEnd: string;
groupBy?: string[];
}) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const report = await client.getSummaryReport(wsId, {
dateRangeStart,
dateRangeEnd,
summaryFilter: { groups: groupBy || ['PROJECT', 'USER'] },
});
const totals = report.totals[0] || { totalTime: 0, totalAmount: 0, entriesCount: 0 };
const summary = {
totalTime: formatDuration(totals.totalTime),
totalAmount: formatMoney(totals.totalAmount),
entriesCount: totals.entriesCount,
breakdown: report.groupOne.map((g) => ({
name: g.name,
duration: formatDuration(g.duration),
amount: formatMoney(g.amount),
children: g.children?.map((c) => ({
name: c.name,
duration: formatDuration(c.duration),
amount: formatMoney(c.amount),
})),
})),
};
return { content: [{ type: 'text' as const, text: JSON.stringify(summary, null, 2) }] };
},
async getDetailedReport({
workspaceId,
dateRangeStart,
dateRangeEnd,
userIds,
projectIds,
}: {
workspaceId?: string;
dateRangeStart: string;
dateRangeEnd: string;
userIds?: string[];
projectIds?: string[];
}) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const report = await client.getDetailedReport(wsId, {
dateRangeStart,
dateRangeEnd,
userIds,
projectIds,
});
const totals = report.totals[0] || { totalTime: 0, totalAmount: 0, entriesCount: 0 };
const result = {
totalTime: formatDuration(totals.totalTime),
totalAmount: formatMoney(totals.totalAmount),
entriesCount: totals.entriesCount,
entries: report.timeentries.map((e) => ({
description: e.description,
user: e.userName,
project: e.projectName,
start: e.timeInterval.start,
end: e.timeInterval.end,
duration: e.timeInterval.duration,
amount: formatMoney(e.amount),
billable: e.billable,
})),
};
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
},
// ─────────────────────────────────────────────────────────
// TAG & CLIENT TOOLS
// ─────────────────────────────────────────────────────────
async getTags({ workspaceId }: { workspaceId?: string }) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const tags = await client.getTags(wsId);
return {
content: [
{ type: 'text' as const, text: JSON.stringify(tags.map((t) => ({ id: t.id, name: t.name })), null, 2) },
],
};
},
async createTag({ workspaceId, name }: { workspaceId?: string; name: string }) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const tag = await client.createTag(wsId, name);
return { content: [{ type: 'text' as const, text: `Created tag "${tag.name}" (ID: ${tag.id})` }] };
},
async getClients({ workspaceId }: { workspaceId?: string }) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const clients = await client.getClients(wsId);
return {
content: [
{ type: 'text' as const, text: JSON.stringify(clients.map((c) => ({ id: c.id, name: c.name })), null, 2) },
],
};
},
// ─────────────────────────────────────────────────────────
// CONVENIENCE TOOLS
// ─────────────────────────────────────────────────────────
async getRunningTimer({ workspaceId }: { workspaceId?: string }) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const user = await client.getCachedUser();
const timer = await client.getRunningTimer(wsId, user.id);
if (!timer) {
return { content: [{ type: 'text' as const, text: 'No timer currently running.' }] };
}
const startTime = new Date(timer.timeInterval.start);
const elapsed = Date.now() - startTime.getTime();
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
id: timer.id,
description: timer.description || '(no description)',
project: timer.projectId,
startedAt: timer.timeInterval.start,
elapsed: formatDuration(elapsed),
billable: timer.billable,
},
null,
2
),
},
],
};
},
async startTimer({
description,
projectId,
workspaceId,
billable,
}: {
description?: string;
projectId?: string;
workspaceId?: string;
billable?: boolean;
}) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
// Check if timer already running
const user = await client.getCachedUser();
const existing = await client.getRunningTimer(wsId, user.id);
if (existing) {
return {
content: [
{
type: 'text' as const,
text: `Timer already running: "${existing.description || '(no description)'}". Stop it first with stop_current_timer.`,
},
],
isError: true,
};
}
const entry = await client.createTimeEntry(wsId, {
start: new Date().toISOString(),
description,
projectId,
billable,
});
return {
content: [
{ type: 'text' as const, text: `Started timer: "${entry.description || '(no description)'}"` },
],
};
},
async stopCurrentTimer({ workspaceId, end }: { workspaceId?: string; end?: string }) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
const user = await client.getCachedUser();
try {
const entry = await client.stopTimer(wsId, user.id, end);
return {
content: [
{
type: 'text' as const,
text: `Stopped timer: "${entry.description || '(no description)'}" (${parseIsoDuration(entry.timeInterval.duration)})`,
},
],
};
} catch (error) {
if (error instanceof Error && error.message.includes('No timer')) {
return { content: [{ type: 'text' as const, text: 'No timer currently running.' }] };
}
throw error;
}
},
async logTime({
description,
duration,
projectId,
workspaceId,
billable,
date,
}: {
description?: string;
duration: string;
projectId?: string;
workspaceId?: string;
billable?: boolean;
date?: string;
}) {
const wsId = workspaceId || (await client.getDefaultWorkspaceId());
// Default to start of today if no date provided
const startDate = date ? new Date(date) : new Date();
if (!date) {
startDate.setHours(9, 0, 0, 0); // Default to 9 AM
}
const endDate = parseDurationToEndTime(startDate, duration);
const entry = await client.createTimeEntry(wsId, {
start: startDate.toISOString(),
end: endDate.toISOString(),
description,
projectId,
billable,
});
return {
content: [
{
type: 'text' as const,
text: `Logged ${duration}: "${entry.description || '(no description)'}"`,
},
],
};
},
};
}
// Tool metadata for registration
export const toolDefinitions = [
// Workspace & User (call these first to get IDs)
{ name: 'get_workspaces', description: 'List all workspaces. Call first if you need workspace IDs.', schema: schemas.getWorkspaces },
{ name: 'get_current_user', description: 'Get current user info including active workspace ID', schema: schemas.getCurrentUser },
{ name: 'get_workspace_users', description: 'List users in workspace. Auto-detects workspace if not specified.', schema: schemas.getWorkspaceUsers },
// Projects (call get_projects first to get project IDs)
{ name: 'get_projects', description: 'List projects. Call first to get project IDs. Auto-detects workspace.', schema: schemas.getProjects },
{ name: 'create_project', description: 'Create a new project. Auto-detects workspace.', schema: schemas.createProject },
{ name: 'get_project_tasks', description: 'List tasks for a project. Use get_projects first to get project ID.', schema: schemas.getProjectTasks },
{ name: 'create_task', description: 'Create a task in a project. Use get_projects first to get project ID.', schema: schemas.createTask },
// Time entries (low-level - prefer convenience tools below)
{ name: 'get_time_entries', description: 'Get time entries. Supports period filter (today, this_week, last_month). Auto-detects workspace/user.', schema: schemas.getTimeEntries },
{ name: 'create_time_entry', description: 'Create time entry (low-level). Prefer start_timer or log_time instead.', schema: schemas.createTimeEntry },
{ name: 'stop_timer', description: 'Stop timer (low-level). Prefer stop_current_timer instead.', schema: schemas.stopTimer },
{ name: 'delete_time_entry', description: 'Delete a time entry by ID. Auto-detects workspace.', schema: schemas.deleteTimeEntry },
{ name: 'update_time_entry', description: 'Update a time entry by ID. Auto-detects workspace.', schema: schemas.updateTimeEntry },
// Reports
{ name: 'get_summary_report', description: 'Summary report grouped by project/user. Auto-detects workspace.', schema: schemas.getSummaryReport },
{ name: 'get_detailed_report', description: 'Detailed report with individual entries. Auto-detects workspace.', schema: schemas.getDetailedReport },
// Tags & Clients
{ name: 'get_tags', description: 'List all tags. Auto-detects workspace.', schema: schemas.getTags },
{ name: 'create_tag', description: 'Create a new tag. Auto-detects workspace.', schema: schemas.createTag },
{ name: 'get_clients', description: 'List all clients. Auto-detects workspace.', schema: schemas.getClients },
// Convenience tools (recommended - auto-detect everything)
{ name: 'get_running_timer', description: 'Check if timer is running. Auto-detects workspace/user.', schema: schemas.getRunningTimer },
{ name: 'start_timer', description: 'Start a timer. Auto-detects workspace. Use get_projects for project IDs.', schema: schemas.startTimer },
{ name: 'stop_current_timer', description: 'Stop running timer. Auto-detects workspace/user.', schema: schemas.stopCurrentTimer },
{ name: 'log_time', description: 'Log completed time with duration like "1h30m". Auto-detects workspace.', schema: schemas.logTime },
];