# Directory Structure
```
├── .env.example
├── .gitignore
├── .nvmrc
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│ ├── clockify-sdk
│ │ ├── entries.ts
│ │ ├── projects.ts
│ │ ├── users.ts
│ │ └── workspaces.ts
│ ├── config
│ │ └── api.ts
│ ├── index.ts
│ ├── tools
│ │ ├── entries.ts
│ │ ├── projects.ts
│ │ ├── users.ts
│ │ └── workspaces.ts
│ ├── types
│ │ └── index.ts
│ └── validation
│ ├── entries
│ │ ├── create-entry-schema.ts
│ │ ├── delete-entry-schema.ts
│ │ ├── edit-entry-schema.ts
│ │ └── find-entry-schema.ts
│ └── projects
│ └── find-project-schema.ts
├── test
│ ├── entries.test.ts
│ ├── setup.ts
│ ├── users.test.ts
│ └── workspaces.test.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
```
v20.17.0
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules
.env
dist
.dist
.smithery
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
CLOCKIFY_API_URL=https://api.clockify.me/api/v1
CLOCKIFY_API_TOKEN=
# Just used in tests
TEST_WORKSPACE_ID=
TEST_USER_ID=
TEST_PROJECT_ID=
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Clockify MCP Server
[](https://smithery.ai/server/@https-eduardo/clockify-mcp-server)
This MCP Server integrates with AI Tools to manage your time entries in Clockify, so you can register your time entries just sending an prompt to LLM.
## Next implementations
- Implement tags for entries
## Using in Claude Desktop
### Installing via Smithery
To install clockify-mcp-server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@https-eduardo/clockify-mcp-server):
```bash
npx -y @smithery/cli install @https-eduardo/clockify-mcp-server --client claude
```
### Installing Manually
First, install tsx globally
`npm i -g tsx`
Then insert the MCP server in `claude_desktop_config`
```json
{
"mcpServers": {
"clockify-time-entries": {
"command": "tsx",
"args": ["ABSOLUTE_PATH/src/index.ts", "--local"],
"env": {
"CLOCKIFY_API_URL": "https://api.clockify.me/api/v1",
"CLOCKIFY_API_TOKEN": "YOUR_CLOCKIFY_API_TOKEN_HERE"
}
}
}
}
```
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
runtime: typescript
```
--------------------------------------------------------------------------------
/src/validation/projects/find-project-schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
export const FindProjectSchema = z.object({
workspaceId: z.string(),
});
```
--------------------------------------------------------------------------------
/src/validation/entries/delete-entry-schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
export const DeleteEntrySchema = z.object({
workspaceId: z.string(),
timeEntryId: z.string(),
});
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"outDir": "./dist",
"target": "ESNext",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
```
--------------------------------------------------------------------------------
/src/validation/entries/create-entry-schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
export const CreateEntrySchema = z.object({
workspaceId: z.string(),
billable: z.boolean(),
description: z.string(),
start: z.coerce.date(),
end: z.coerce.date(),
projectId: z.string().optional(),
});
```
--------------------------------------------------------------------------------
/src/clockify-sdk/users.ts:
--------------------------------------------------------------------------------
```typescript
import { AxiosInstance } from "axios";
import { api } from "../config/api";
function UsersService(api: AxiosInstance) {
async function getCurrent() {
return api.get("user");
}
return { getCurrent };
}
export const usersService = UsersService(api);
```
--------------------------------------------------------------------------------
/src/validation/entries/find-entry-schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
export const FindEntrySchema = z.object({
workspaceId: z.string(),
userId: z.string(),
description: z.string().optional(),
start: z.coerce.date().optional(),
end: z.coerce.date().optional(),
project: z.string().optional(),
});
```
--------------------------------------------------------------------------------
/src/clockify-sdk/workspaces.ts:
--------------------------------------------------------------------------------
```typescript
import { AxiosInstance } from "axios";
import { api } from "../config/api";
function WorkspacesService(api: AxiosInstance) {
async function fetchAll() {
return api.get(`workspaces`);
}
return { fetchAll };
}
export const workspacesService = WorkspacesService(api);
```
--------------------------------------------------------------------------------
/src/clockify-sdk/projects.ts:
--------------------------------------------------------------------------------
```typescript
import { AxiosInstance } from "axios";
import { api } from "../config/api";
function ProjectsService(api: AxiosInstance) {
async function fetchAll(workspaceId: string) {
return api.get(`workspaces/${workspaceId}/projects?archived=false`);
}
return { fetchAll };
}
export const projectsService = ProjectsService(api);
```
--------------------------------------------------------------------------------
/src/validation/entries/edit-entry-schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
export const EditEntrySchema = z.object({
workspaceId: z.string(),
timeEntryId: z.string(),
billable: z.boolean().optional(),
description: z.string().optional(),
start: z.union([z.coerce.date(), z.undefined()]).optional(),
end: z.union([z.coerce.date(), z.undefined()]).optional(),
projectId: z.string().optional(),
});
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "clockify-mcp-server",
"version": "1.1.1",
"main": "index.js",
"module": "./src/index.ts",
"type": "module",
"files": [
"dist"
],
"scripts": {
"dev": "npx @smithery/cli dev",
"build": "npx @smithery/cli build"
},
"license": "MIT",
"engines": {
"node": ">=20.0.0"
},
"devDependencies": {
"@smithery/cli": "^1.2.4",
"@types/node": "^20.19.7",
"ts-node-dev": "^2.0.0",
"tsx": "^4.20.3",
"typescript": "^5.8.3"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"axios": "^1.8.4",
"dotenv": "^16.5.0",
"zod": "^3.24.2"
}
}
```
--------------------------------------------------------------------------------
/test/users.test.ts:
--------------------------------------------------------------------------------
```typescript
import { after, describe, it } from "node:test";
import { createMcpClient, TEST_WORKSPACE_ID, TEST_USER_ID } from "./setup";
import assert from "node:assert";
import { ClockifyUser, McpResponse } from "../src/types";
describe("Users MCP Tests", async () => {
const client = await createMcpClient();
after(async () => {
await client.close();
});
it("Retrieve current user info", async () => {
const response = (await client.callTool({
name: "get-current-user",
})) as McpResponse;
const user: ClockifyUser = JSON.parse(response.content[0].text as string);
assert(user.id === TEST_USER_ID);
});
});
```
--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------
```typescript
import dotenv from "dotenv";
dotenv.config();
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
export const TEST_WORKSPACE_ID = process.env.TEST_WORKSPACE_ID;
export const TEST_USER_ID = process.env.TEST_USER_ID;
export const TEST_PROJECT_ID = process.env.TEST_PROJECT_ID;
export async function createMcpClient() {
const transport = new StdioClientTransport({
command: "ts-node",
args: ["src/index.ts"],
});
const client = new Client({
name: "clockify-test-mcp-client",
version: "1.1.1",
});
await client.connect(transport);
return client;
}
```
--------------------------------------------------------------------------------
/src/tools/users.ts:
--------------------------------------------------------------------------------
```typescript
import { TOOLS_CONFIG } from "../config/api";
import { usersService } from "../clockify-sdk/users";
import {
ClockifyUser,
McpResponse,
McpToolConfigWithoutParameters,
} from "../types";
export const getCurrentUserTool: McpToolConfigWithoutParameters = {
name: TOOLS_CONFIG.users.current.name,
description: TOOLS_CONFIG.users.current.description,
handler: async (): Promise<McpResponse> => {
const response = await usersService.getCurrent();
const user: ClockifyUser = {
id: response.data.id,
name: response.data.name,
email: response.data.email,
};
return {
content: [
{
type: "text",
text: JSON.stringify(user),
},
],
};
},
};
```
--------------------------------------------------------------------------------
/src/tools/workspaces.ts:
--------------------------------------------------------------------------------
```typescript
import { TOOLS_CONFIG } from "../config/api";
import { workspacesService } from "../clockify-sdk/workspaces";
import {
ClockifyWorkspace,
McpResponse,
McpToolConfigWithoutParameters,
} from "../types";
export const findWorkspacesTool: McpToolConfigWithoutParameters = {
name: TOOLS_CONFIG.workspaces.list.name,
description: TOOLS_CONFIG.workspaces.list.description,
handler: async (): Promise<McpResponse> => {
const response = await workspacesService.fetchAll();
const workspaces = response.data.map((workspace: ClockifyWorkspace) => ({
name: workspace.name,
id: workspace.id,
}));
return {
content: [
{
type: "text",
text: JSON.stringify(workspaces),
},
],
};
},
};
```
--------------------------------------------------------------------------------
/test/workspaces.test.ts:
--------------------------------------------------------------------------------
```typescript
import { after, describe, it } from "node:test";
import assert from "node:assert";
import { createMcpClient, TEST_WORKSPACE_ID } from "./setup";
import { ClockifyWorkspace, McpResponse } from "../src/types";
describe("Workspaces MCP Tests", async () => {
const client = await createMcpClient();
after(async () => {
await client.close();
});
it("should list all user workspaces", async () => {
const result = (await client.callTool({
name: "get-workspaces",
})) as McpResponse;
const workspaces: ClockifyWorkspace[] = JSON.parse(
result.content[0].text as string
);
assert(workspaces.length > 0);
assert(
workspaces.find(
(workspace: ClockifyWorkspace) => workspace.id === TEST_WORKSPACE_ID
)
);
});
});
```
--------------------------------------------------------------------------------
/src/tools/projects.ts:
--------------------------------------------------------------------------------
```typescript
import { projectsService } from "../clockify-sdk/projects";
import { TOOLS_CONFIG } from "../config/api";
import { z } from "zod";
import { McpResponse, McpToolConfig, TFindProjectSchema } from "../types";
export const findProjectTool: McpToolConfig = {
name: TOOLS_CONFIG.projects.list.name,
description: TOOLS_CONFIG.projects.list.description,
parameters: {
workspaceId: z
.string()
.describe(
"The ID of the workspace that you need to get the projects from"
),
},
handler: async ({
workspaceId,
}: TFindProjectSchema): Promise<McpResponse> => {
if (!workspaceId && typeof workspaceId === "string")
throw new Error("Workspace ID required to fetch projects");
const response = await projectsService.fetchAll(workspaceId as string);
const projects = response.data.map((project: any) => ({
name: project.name,
clientName: project.clientName,
id: project.id,
}));
return {
content: [
{
type: "text",
text: JSON.stringify(projects),
},
],
};
},
};
```
--------------------------------------------------------------------------------
/src/config/api.ts:
--------------------------------------------------------------------------------
```typescript
import axios from "axios";
export const api = axios.create({
baseURL: process.env.CLOCKIFY_API_URL || 'https://api.clockify.me/api/v1',
headers: {
"X-Api-Key": `${process.env.CLOCKIFY_API_TOKEN}`,
},
});
export const SERVER_CONFIG = {
name: "Clockify MCP Server",
version: "1.0.0",
description:
"A service that integrates with Clockify API to manage time entries",
};
export const TOOLS_CONFIG = {
workspaces: {
list: {
name: "get-workspaces",
description:
"Get user available workspaces id and name, a workspace is required to manage time entries",
},
},
projects: {
list: {
name: "get-projects",
description:
"Get workspace projects id and name, the projects can be associated with time entries",
},
},
users: {
current: {
name: "get-current-user",
description:
"Get the current user id and name, to search for entries is required to have the user id",
},
},
entries: {
create: {
name: "create-time-entry",
description:
"Register a new time entry of a task or break in a workspace",
},
list: {
name: "list-time-entries",
description: "Get registered time entries from a workspace",
},
delete: {
name: "delete-time-entry",
description: "Delete a specific time entry from a workspace",
},
edit: {
name: "edit-time-entry",
description: "Edit an existing time entry in a workspace",
},
},
};
```
--------------------------------------------------------------------------------
/src/clockify-sdk/entries.ts:
--------------------------------------------------------------------------------
```typescript
import { AxiosInstance } from "axios";
import { api } from "../config/api";
import {
TCreateEntrySchema,
TFindEntrySchema,
TDeleteEntrySchema,
TEditEntrySchema,
} from "../types";
import { URLSearchParams } from "node:url";
function EntriesService(api: AxiosInstance) {
async function create(entry: TCreateEntrySchema) {
const body = {
...entry,
workspaceId: undefined,
};
return api.post(`workspaces/${entry.workspaceId}/time-entries`, body);
}
async function find(filters: TFindEntrySchema) {
const searchParams = new URLSearchParams();
if (filters.description)
searchParams.append("description", filters.description);
if (filters.start)
searchParams.append("start", filters.start.toISOString());
if (filters.end) searchParams.append("end", filters.end.toISOString());
if (filters.project) searchParams.append("project", filters.project);
return api.get(
`https://api.clockify.me/api/v1/workspaces/${filters.workspaceId}/user/${
filters.userId
}/time-entries?${searchParams.toString()}`
);
}
async function deleteEntry(params: TDeleteEntrySchema) {
return api.delete(
`workspaces/${params.workspaceId}/time-entries/${params.timeEntryId}`
);
}
async function update(params: TEditEntrySchema) {
const body = {
...params,
workspaceId: undefined,
timeEntryId: undefined,
};
return api.put(
`workspaces/${params.workspaceId}/time-entries/${params.timeEntryId}`,
body
);
}
async function getById(workspaceId: string, timeEntryId: string) {
return api.get(`workspaces/${workspaceId}/time-entries/${timeEntryId}`);
}
return { create, find, deleteEntry, update, getById };
}
export const entriesService = EntriesService(api);
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import dotenv from "dotenv";
dotenv.config();
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { api, SERVER_CONFIG } from "./config/api";
import {
createEntryTool,
deleteEntryTool,
editEntryTool,
listEntriesTool,
} from "./tools/entries";
import { findProjectTool } from "./tools/projects";
import { getCurrentUserTool } from "./tools/users";
import { findWorkspacesTool } from "./tools/workspaces";
import { z } from "zod";
import { argv } from "process";
export const configSchema = z.object({
clockifyApiToken: z.string().describe("Clockify API Token"),
});
const server = new McpServer(SERVER_CONFIG);
export default function createStatelessServer({
config,
}: {
config: z.infer<typeof configSchema>;
}) {
api.defaults.headers.Authorization = `Bearer ${config.clockifyApiToken}`;
server.tool(
createEntryTool.name,
createEntryTool.description,
createEntryTool.parameters,
createEntryTool.handler
);
server.tool(
findProjectTool.name,
findProjectTool.description,
findProjectTool.parameters,
findProjectTool.handler
);
server.tool(
listEntriesTool.name,
listEntriesTool.description,
listEntriesTool.parameters,
listEntriesTool.handler
);
server.tool(
getCurrentUserTool.name,
getCurrentUserTool.description,
getCurrentUserTool.handler
);
server.tool(
findWorkspacesTool.name,
findWorkspacesTool.description,
findWorkspacesTool.handler
);
server.tool(
deleteEntryTool.name,
deleteEntryTool.description,
deleteEntryTool.parameters,
deleteEntryTool.handler
);
server.tool(
editEntryTool.name,
editEntryTool.description,
editEntryTool.parameters,
editEntryTool.handler
);
return server.server;
}
(() => {
if (argv.find((flag) => flag === "--local")) {
createStatelessServer({
config: {
clockifyApiToken: process.env.CLOCKIFY_API_TOKEN as string,
},
});
const transport = new StdioServerTransport();
server.connect(transport);
}
})();
```
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { CreateEntrySchema } from "../validation/entries/create-entry-schema";
import { FindEntrySchema } from "../validation/entries/find-entry-schema";
import { DeleteEntrySchema } from "../validation/entries/delete-entry-schema";
import { EditEntrySchema } from "../validation/entries/edit-entry-schema";
import {
ReadResourceTemplateCallback,
ResourceMetadata,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp";
import { FindProjectSchema } from "../validation/projects/find-project-schema";
export type TCreateEntrySchema = z.infer<typeof CreateEntrySchema>;
export type TFindEntrySchema = z.infer<typeof FindEntrySchema>;
export type TDeleteEntrySchema = z.infer<typeof DeleteEntrySchema>;
export type TEditEntrySchema = z.infer<typeof EditEntrySchema>;
export type TFindProjectSchema = z.infer<typeof FindProjectSchema>;
export interface ClockifyWorkspace {
id: string;
name: string;
}
export interface ClockifyUser {
id: string;
name: string;
email: string;
}
export interface McpToolConfig {
name: string;
description: string;
parameters: Record<string, any>;
handler: (params: any) => Promise<McpResponse>;
}
export type McpToolConfigWithoutParameters = Omit<McpToolConfig, "parameters">;
export interface McpTextContent {
type: "text";
text: string;
[key: string]: unknown;
}
export interface McpImageContent {
type: "image";
data: string;
mimeType: string;
[key: string]: unknown;
}
export interface McpResourceConfig {
name: string;
template: ResourceTemplate;
metadata: ResourceMetadata;
handler: ReadResourceTemplateCallback;
}
export interface McpResourceContent {
type: "resource";
resource:
| {
text: string;
uri: string;
mimeType?: string;
[key: string]: unknown;
}
| {
uri: string;
blob: string;
mimeType?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}
export type McpContent = McpTextContent | McpImageContent | McpResourceContent;
export interface McpResponse {
content: McpContent[];
_meta?: Record<string, unknown>;
isError?: boolean;
[key: string]: unknown;
}
```
--------------------------------------------------------------------------------
/test/entries.test.ts:
--------------------------------------------------------------------------------
```typescript
import { after, describe, it } from "node:test";
import { createMcpClient, TEST_WORKSPACE_ID, TEST_PROJECT_ID } from "./setup";
import { McpResponse } from "../src/types";
import assert from "node:assert";
let createdEntryId: string;
describe("Entries MCP Tests", async () => {
const client = await createMcpClient();
after(async () => {
await client.close();
});
it("Create a billable time entry without project", async () => {
const dateOneHourBefore = new Date();
dateOneHourBefore.setHours(dateOneHourBefore.getHours() - 1);
const currentDate = new Date();
const response = (await client.callTool({
name: "create-time-entry",
arguments: {
workspaceId: TEST_WORKSPACE_ID,
billable: true,
description: "MCP Test Entry",
start: dateOneHourBefore,
end: currentDate,
},
})) as McpResponse;
assert(
(response.content[0].text as string).startsWith(
"Time entry created successfully"
)
);
});
it("Create a time entry with project", async () => {
const dateTwoHoursBefore = new Date();
dateTwoHoursBefore.setHours(dateTwoHoursBefore.getHours() - 2);
const dateOneHourBefore = new Date();
dateOneHourBefore.setHours(dateOneHourBefore.getHours() - 1);
const response = (await client.callTool({
name: "create-time-entry",
arguments: {
workspaceId: TEST_WORKSPACE_ID,
billable: false,
description: "MCP Test Entry with Project",
start: dateTwoHoursBefore,
end: dateOneHourBefore,
projectId: TEST_PROJECT_ID,
},
})) as McpResponse;
assert(
(response.content[0].text as string).startsWith(
"Time entry created successfully"
)
);
const match = (response.content[0].text as string).match(/ID: ([^\s]+)/);
if (match) {
createdEntryId = match[1];
}
});
it("Edit a time entry", async () => {
if (!createdEntryId) {
throw new Error("No entry ID available for editing");
}
const response = (await client.callTool({
name: "edit-time-entry",
arguments: {
workspaceId: TEST_WORKSPACE_ID,
timeEntryId: createdEntryId,
description: "MCP Test Entry Edited",
billable: false,
},
})) as McpResponse;
assert(
(response.content[0].text as string).startsWith(
"Time entry updated successfully"
)
);
});
it("Delete a time entry", async () => {
if (!createdEntryId) {
throw new Error("No entry ID available for deletion");
}
const response = (await client.callTool({
name: "delete-time-entry",
arguments: {
workspaceId: TEST_WORKSPACE_ID,
timeEntryId: createdEntryId,
},
})) as McpResponse;
assert(
(response.content[0].text as string).startsWith("Time entry with ID")
);
assert(
(response.content[0].text as string).includes("was deleted successfully")
);
});
});
```
--------------------------------------------------------------------------------
/src/tools/entries.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { TOOLS_CONFIG } from "../config/api";
import { entriesService } from "../clockify-sdk/entries";
import {
McpResponse,
McpToolConfig,
TCreateEntrySchema,
TFindEntrySchema,
TDeleteEntrySchema,
TEditEntrySchema,
} from "../types";
export const createEntryTool: McpToolConfig = {
name: TOOLS_CONFIG.entries.create.name,
description: TOOLS_CONFIG.entries.create.description,
parameters: {
workspaceId: z
.string()
.describe("The id of the workspace that gonna be saved the time entry"),
billable: z
.boolean()
.describe("If the task is billable or not")
.optional()
.default(true),
description: z.string().describe("The description of the time entry"),
start: z.coerce.date().describe("The start of the time entry"),
end: z.coerce.date().describe("The end of the time entry"),
projectId: z
.string()
.optional()
.describe("The id of the project associated with this time entry"),
},
handler: async (params: TCreateEntrySchema): Promise<McpResponse> => {
try {
const result = await entriesService.create(params);
const entryInfo = `Time entry created successfully. ID: ${result.data.id} Name: ${result.data.description}`;
return {
content: [
{
type: "text",
text: entryInfo,
},
],
};
} catch (error: any) {
throw new Error(`Failed to create entry: ${error.message}`);
}
},
};
export const listEntriesTool: McpToolConfig = {
name: TOOLS_CONFIG.entries.list.name,
description: TOOLS_CONFIG.entries.list.description,
parameters: {
workspaceId: z
.string()
.describe("The id of the workspace that gonna search for the entries"),
userId: z
.string()
.describe(
"The id of the user that gonna have the entries searched, default is the current user id"
),
description: z
.string()
.optional()
.describe("The time entry description to search for"),
start: z.coerce
.date()
.optional()
.describe("Start time of the entry to search for"),
end: z.coerce
.date()
.optional()
.describe("End time of the entry to search for"),
project: z
.string()
.optional()
.describe("The id of the project to search for entries"),
},
handler: async (params: TFindEntrySchema) => {
try {
const result = await entriesService.find(params);
const formmatedResults = result.data.map((entry: any) => ({
id: entry.id,
description: entry.description,
duration: entry.duration,
start: entry.start,
end: entry.end,
}));
return {
content: [
{
type: "text",
text: JSON.stringify(formmatedResults),
},
],
};
} catch (error: any) {
throw new Error(`Failed to retrieve entries: ${error.message}`);
}
},
};
export const deleteEntryTool: McpToolConfig = {
name: TOOLS_CONFIG.entries.delete.name,
description: TOOLS_CONFIG.entries.delete.description,
parameters: {
workspaceId: z
.string()
.describe("The id of the workspace where the time entry is located"),
timeEntryId: z.string().describe("The id of the time entry to be deleted"),
},
handler: async (params: TDeleteEntrySchema): Promise<McpResponse> => {
try {
await entriesService.deleteEntry(params);
return {
content: [
{
type: "text",
text: `Time entry with ID ${params.timeEntryId} was deleted successfully.`,
},
],
};
} catch (error: any) {
throw new Error(`Failed to delete entry: ${error.message}`);
}
},
};
export const editEntryTool: McpToolConfig = {
name: TOOLS_CONFIG.entries.edit.name,
description: TOOLS_CONFIG.entries.edit.description,
parameters: {
workspaceId: z
.string()
.describe("The id of the workspace where the time entry is located"),
timeEntryId: z.string().describe("The id of the time entry to be edited"),
billable: z.boolean().describe("If the task is billable or not").optional(),
description: z
.string()
.describe("The description of the time entry")
.optional(),
start: z.coerce.date().describe("The start of the time entry").optional(),
end: z.coerce.date().describe("The end of the time entry").optional(),
projectId: z
.string()
.optional()
.describe("The id of the project associated with this time entry"),
},
handler: async (params: TEditEntrySchema): Promise<McpResponse> => {
try {
let start = params.start;
if (!start) {
const current = await entriesService.getById(
params.workspaceId,
params.timeEntryId
);
start = new Date(current.data.timeInterval.start);
}
const result = await entriesService.update({
...params,
start,
});
const entryInfo = `Time entry updated successfully. ID: ${result.data.id} Name: ${result.data.description}`;
return {
content: [
{
type: "text",
text: entryInfo,
},
],
};
} catch (error: any) {
throw new Error(`Failed to edit entry: ${error.message}`);
}
},
};
```