Midday / apps /api /src /mcp /tools /tracker.ts
Jules
Final deployment with all fixes and verified content
c09f67c
import {
deleteTrackerEntrySchema,
startTimerSchema,
stopTimerSchema,
upsertTrackerEntriesSchema,
} from "@api/schemas/tracker-entries";
import {
deleteTrackerProjectSchema,
getTrackerProjectByIdSchema,
getTrackerProjectsSchema,
upsertTrackerProjectSchema,
} from "@api/schemas/tracker-projects";
import {
deleteTrackerEntry,
deleteTrackerProject,
getTimerStatus,
getTrackerEntryById,
getTrackerProjectById,
getTrackerProjects,
getTrackerRecordsByRange,
startTimer,
stopTimer,
upsertTrackerEntries,
upsertTrackerProject,
} from "@midday/db/queries";
import { z } from "zod";
import { hasScope, READ_ONLY_ANNOTATIONS, type RegisterTools } from "../types";
// Annotations for write operations
const WRITE_ANNOTATIONS = {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
} as const;
// Annotations for destructive operations
const DESTRUCTIVE_ANNOTATIONS = {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
} as const;
export const registerTrackerTools: RegisterTools = (server, ctx) => {
const { db, teamId } = ctx;
// Check scopes
const hasProjectReadScope = hasScope(ctx, "tracker-projects.read");
const hasProjectWriteScope = hasScope(ctx, "tracker-projects.write");
const hasEntryReadScope = hasScope(ctx, "tracker-entries.read");
const hasEntryWriteScope = hasScope(ctx, "tracker-entries.write");
// Skip if user has no tracker scopes
if (
!hasProjectReadScope &&
!hasProjectWriteScope &&
!hasEntryReadScope &&
!hasEntryWriteScope
) {
return;
}
// ==========================================
// TRACKER PROJECT TOOLS
// ==========================================
// List projects (read scope)
if (hasProjectReadScope) {
server.registerTool(
"tracker_projects_list",
{
title: "List Tracker Projects",
description:
"List time tracking projects with filtering by status, customer, and date range",
inputSchema: getTrackerProjectsSchema.shape,
annotations: READ_ONLY_ANNOTATIONS,
},
async (params) => {
const result = await getTrackerProjects(db, {
teamId,
cursor: params.cursor ?? null,
pageSize: params.pageSize ?? 25,
q: params.q ?? null,
status: params.status ?? null,
customers: params.customers ?? null,
start: params.start ?? null,
end: params.end ?? null,
sort: params.sort ?? null,
tags: params.tags ?? null,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
server.registerTool(
"tracker_projects_get",
{
title: "Get Tracker Project",
description: "Get a specific tracker project by its ID",
inputSchema: {
id: getTrackerProjectByIdSchema.shape.id,
},
annotations: READ_ONLY_ANNOTATIONS,
},
async ({ id }) => {
const result = await getTrackerProjectById(db, { id, teamId });
if (!result) {
return {
content: [{ type: "text", text: "Project not found" }],
isError: true,
};
}
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
}
// Create/Update project (write scope)
if (hasProjectWriteScope) {
server.registerTool(
"tracker_projects_create",
{
title: "Create Tracker Project",
description:
"Create a new time tracking project. Specify name, optional description, rate, currency, estimate, and customer.",
inputSchema: {
name: upsertTrackerProjectSchema.shape.name,
description: upsertTrackerProjectSchema.shape.description,
estimate: upsertTrackerProjectSchema.shape.estimate,
billable: upsertTrackerProjectSchema.shape.billable,
rate: upsertTrackerProjectSchema.shape.rate,
currency: upsertTrackerProjectSchema.shape.currency,
customerId: upsertTrackerProjectSchema.shape.customerId,
tags: upsertTrackerProjectSchema.shape.tags,
},
annotations: WRITE_ANNOTATIONS,
},
async (params) => {
const result = await upsertTrackerProject(db, {
teamId,
name: params.name,
description: params.description,
estimate: params.estimate,
billable: params.billable,
rate: params.rate,
currency: params.currency,
customerId: params.customerId,
tags: params.tags,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
server.registerTool(
"tracker_projects_update",
{
title: "Update Tracker Project",
description:
"Update an existing time tracking project. Provide the project ID and the fields to update.",
inputSchema: {
id: z.string().uuid().describe("The ID of the project to update"),
name: upsertTrackerProjectSchema.shape.name.optional(),
description: upsertTrackerProjectSchema.shape.description,
estimate: upsertTrackerProjectSchema.shape.estimate,
billable: upsertTrackerProjectSchema.shape.billable,
rate: upsertTrackerProjectSchema.shape.rate,
currency: upsertTrackerProjectSchema.shape.currency,
customerId: upsertTrackerProjectSchema.shape.customerId,
tags: upsertTrackerProjectSchema.shape.tags,
},
annotations: WRITE_ANNOTATIONS,
},
async (params) => {
// First check if project exists
const existing = await getTrackerProjectById(db, {
id: params.id,
teamId,
});
if (!existing) {
return {
content: [{ type: "text", text: "Project not found" }],
isError: true,
};
}
// Map existing tags from { id, name } to { id, value } format for upsert
const existingTags = existing.tags?.map((tag) => ({
id: tag.id,
value: tag.name ?? "",
}));
const result = await upsertTrackerProject(db, {
id: params.id,
teamId,
name: params.name ?? existing.name ?? "",
description: params.description ?? existing.description,
estimate: params.estimate ?? existing.estimate,
billable: params.billable ?? existing.billable,
rate: params.rate ?? existing.rate,
currency: params.currency ?? existing.currency,
customerId: params.customerId ?? existing.customerId,
tags: params.tags ?? existingTags,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
server.registerTool(
"tracker_projects_delete",
{
title: "Delete Tracker Project",
description:
"Delete a tracker project by its ID. This will also delete all associated time entries.",
inputSchema: {
id: deleteTrackerProjectSchema.shape.id,
},
annotations: DESTRUCTIVE_ANNOTATIONS,
},
async ({ id }) => {
const result = await deleteTrackerProject(db, { id, teamId });
if (!result) {
return {
content: [
{ type: "text", text: "Project not found or already deleted" },
],
isError: true,
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{ success: true, deletedId: result.id },
null,
2,
),
},
],
};
},
);
}
// ==========================================
// TRACKER ENTRY TOOLS
// ==========================================
// List entries (read scope)
if (hasEntryReadScope) {
server.registerTool(
"tracker_entries_list",
{
title: "List Tracker Entries",
description:
"List time tracking entries with filtering by project and date range",
inputSchema: {
from: z.string().describe("Start date (YYYY-MM-DD) - required"),
to: z.string().describe("End date (YYYY-MM-DD) - required"),
projectId: z
.string()
.uuid()
.optional()
.describe("Filter by project ID"),
},
annotations: READ_ONLY_ANNOTATIONS,
},
async (params) => {
const result = await getTrackerRecordsByRange(db, {
teamId,
from: params.from,
to: params.to,
projectId: params.projectId,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
server.registerTool(
"tracker_timer_status",
{
title: "Get Timer Status",
description:
"Get the current timer status including whether a timer is running and elapsed time",
inputSchema: {
assignedId: z
.string()
.uuid()
.optional()
.nullable()
.describe("User ID to check timer for (optional)"),
},
annotations: READ_ONLY_ANNOTATIONS,
},
async (params) => {
const result = await getTimerStatus(db, {
teamId,
assignedId: params.assignedId,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
}
// Create/Update/Delete entries (write scope)
if (hasEntryWriteScope) {
server.registerTool(
"tracker_entries_create",
{
title: "Create Tracker Entry",
description:
"Create a new time tracking entry. Specify project, dates, start/stop times, and duration.",
inputSchema: {
projectId: upsertTrackerEntriesSchema.shape.projectId,
dates: upsertTrackerEntriesSchema.shape.dates,
start: upsertTrackerEntriesSchema.shape.start,
stop: upsertTrackerEntriesSchema.shape.stop,
duration: upsertTrackerEntriesSchema.shape.duration,
description: upsertTrackerEntriesSchema.shape.description,
assignedId: upsertTrackerEntriesSchema.shape.assignedId,
},
annotations: WRITE_ANNOTATIONS,
},
async (params) => {
const result = await upsertTrackerEntries(db, {
teamId,
projectId: params.projectId,
dates: params.dates,
start: params.start,
stop: params.stop,
duration: params.duration,
description: params.description,
assignedId: params.assignedId,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
server.registerTool(
"tracker_entries_update",
{
title: "Update Tracker Entry",
description:
"Update an existing time tracking entry. Provide the entry ID and only the fields you want to update.",
inputSchema: {
id: z.string().uuid().describe("The ID of the entry to update"),
projectId: upsertTrackerEntriesSchema.shape.projectId.optional(),
start: upsertTrackerEntriesSchema.shape.start.optional(),
stop: upsertTrackerEntriesSchema.shape.stop.optional(),
duration: upsertTrackerEntriesSchema.shape.duration.optional(),
description: upsertTrackerEntriesSchema.shape.description,
assignedId: upsertTrackerEntriesSchema.shape.assignedId,
},
annotations: WRITE_ANNOTATIONS,
},
async (params) => {
// Fetch the existing entry
const existing = await getTrackerEntryById(db, {
id: params.id,
teamId,
});
if (!existing) {
return {
content: [{ type: "text", text: "Entry not found" }],
isError: true,
};
}
// Validate that we have required fields from either params or existing
const projectId = params.projectId ?? existing.projectId;
const start = params.start ?? existing.start;
const stop = params.stop ?? existing.stop;
if (!projectId || !start || !stop) {
return {
content: [
{
type: "text",
text: "Entry is missing required fields (projectId, start, or stop). Please provide them.",
},
],
isError: true,
};
}
const result = await upsertTrackerEntries(db, {
id: params.id,
teamId,
projectId,
dates: existing.date ? [existing.date] : [],
start,
stop,
duration: params.duration ?? existing.duration ?? 0,
description: params.description ?? existing.description,
assignedId: params.assignedId ?? existing.assignedId,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
server.registerTool(
"tracker_entries_delete",
{
title: "Delete Tracker Entry",
description: "Delete a time tracking entry by its ID",
inputSchema: {
id: deleteTrackerEntrySchema.shape.id,
},
annotations: DESTRUCTIVE_ANNOTATIONS,
},
async ({ id }) => {
const result = await deleteTrackerEntry(db, { id, teamId });
if (!result) {
return {
content: [
{ type: "text", text: "Entry not found or already deleted" },
],
isError: true,
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{ success: true, deletedId: result.id },
null,
2,
),
},
],
};
},
);
// Timer tools
server.registerTool(
"tracker_timer_start",
{
title: "Start Timer",
description:
"Start a new timer for a project. Any currently running timer will be stopped automatically.",
inputSchema: {
projectId: startTimerSchema.shape.projectId,
description: startTimerSchema.shape.description,
assignedId: startTimerSchema.shape.assignedId,
start: startTimerSchema.shape.start,
},
annotations: WRITE_ANNOTATIONS,
},
async (params) => {
const result = await startTimer(db, {
teamId,
projectId: params.projectId,
description: params.description,
assignedId: params.assignedId,
start: params.start,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
);
server.registerTool(
"tracker_timer_stop",
{
title: "Stop Timer",
description:
"Stop the current running timer. Optionally specify a specific entry ID to stop.",
inputSchema: {
entryId: stopTimerSchema.shape.entryId,
assignedId: stopTimerSchema.shape.assignedId,
stop: stopTimerSchema.shape.stop,
},
annotations: WRITE_ANNOTATIONS,
},
async (params) => {
try {
const result = await stopTimer(db, {
teamId,
entryId: params.entryId,
assignedId: params.assignedId,
stop: params.stop,
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
return {
content: [
{
type: "text",
text:
error instanceof Error
? error.message
: "Failed to stop timer",
},
],
isError: true,
};
}
},
);
}
};