#
tokens: 9140/50000 27/27 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | v20.17.0
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules
2 | .env
3 | dist
4 | .dist
5 | .smithery
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
1 | CLOCKIFY_API_URL=https://api.clockify.me/api/v1
2 | CLOCKIFY_API_TOKEN=
3 | # Just used in tests
4 | TEST_WORKSPACE_ID=
5 | TEST_USER_ID=
6 | TEST_PROJECT_ID=
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Clockify MCP Server
 2 | 
 3 | [![smithery badge](https://smithery.ai/badge/@https-eduardo/clockify-mcp-server)](https://smithery.ai/server/@https-eduardo/clockify-mcp-server)
 4 | 
 5 | 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.
 6 | 
 7 | ## Next implementations
 8 | 
 9 | - Implement tags for entries
10 | 
11 | ## Using in Claude Desktop
12 | 
13 | ### Installing via Smithery
14 | 
15 | To install clockify-mcp-server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@https-eduardo/clockify-mcp-server):
16 | 
17 | ```bash
18 | npx -y @smithery/cli install @https-eduardo/clockify-mcp-server --client claude
19 | ```
20 | 
21 | ### Installing Manually
22 | 
23 | First, install tsx globally
24 | 
25 | `npm i -g tsx`
26 | 
27 | Then insert the MCP server in `claude_desktop_config`
28 | 
29 | ```json
30 | {
31 |   "mcpServers": {
32 |     "clockify-time-entries": {
33 |       "command": "tsx",
34 |       "args": ["ABSOLUTE_PATH/src/index.ts", "--local"],
35 |       "env": {
36 |         "CLOCKIFY_API_URL": "https://api.clockify.me/api/v1",
37 |         "CLOCKIFY_API_TOKEN": "YOUR_CLOCKIFY_API_TOKEN_HERE"
38 |       }
39 |     }
40 |   }
41 | }
42 | ```
43 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
1 | runtime: typescript
2 | 
```

--------------------------------------------------------------------------------
/src/validation/projects/find-project-schema.ts:
--------------------------------------------------------------------------------

```typescript
1 | import { z } from "zod";
2 | 
3 | export const FindProjectSchema = z.object({
4 |   workspaceId: z.string(),
5 | });
6 | 
```

--------------------------------------------------------------------------------
/src/validation/entries/delete-entry-schema.ts:
--------------------------------------------------------------------------------

```typescript
1 | import { z } from "zod";
2 | 
3 | export const DeleteEntrySchema = z.object({
4 |   workspaceId: z.string(),
5 |   timeEntryId: z.string(),
6 | });
7 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "outDir": "./dist",
 4 |     "target": "ESNext",
 5 |     "module": "commonjs",
 6 |     "esModuleInterop": true,
 7 |     "forceConsistentCasingInFileNames": true,
 8 |     "strict": true,
 9 |     "skipLibCheck": true
10 |   }
11 | }
12 | 
```

--------------------------------------------------------------------------------
/src/validation/entries/create-entry-schema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | 
 3 | export const CreateEntrySchema = z.object({
 4 |   workspaceId: z.string(),
 5 |   billable: z.boolean(),
 6 |   description: z.string(),
 7 |   start: z.coerce.date(),
 8 |   end: z.coerce.date(),
 9 |   projectId: z.string().optional(),
10 | });
11 | 
```

--------------------------------------------------------------------------------
/src/clockify-sdk/users.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { AxiosInstance } from "axios";
 2 | import { api } from "../config/api";
 3 | 
 4 | function UsersService(api: AxiosInstance) {
 5 |   async function getCurrent() {
 6 |     return api.get("user");
 7 |   }
 8 | 
 9 |   return { getCurrent };
10 | }
11 | 
12 | export const usersService = UsersService(api);
13 | 
```

--------------------------------------------------------------------------------
/src/validation/entries/find-entry-schema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | 
 3 | export const FindEntrySchema = z.object({
 4 |   workspaceId: z.string(),
 5 |   userId: z.string(),
 6 |   description: z.string().optional(),
 7 |   start: z.coerce.date().optional(),
 8 |   end: z.coerce.date().optional(),
 9 |   project: z.string().optional(),
10 | });
11 | 
```

--------------------------------------------------------------------------------
/src/clockify-sdk/workspaces.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { AxiosInstance } from "axios";
 2 | import { api } from "../config/api";
 3 | 
 4 | function WorkspacesService(api: AxiosInstance) {
 5 |   async function fetchAll() {
 6 |     return api.get(`workspaces`);
 7 |   }
 8 | 
 9 |   return { fetchAll };
10 | }
11 | 
12 | export const workspacesService = WorkspacesService(api);
13 | 
```

--------------------------------------------------------------------------------
/src/clockify-sdk/projects.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { AxiosInstance } from "axios";
 2 | import { api } from "../config/api";
 3 | 
 4 | function ProjectsService(api: AxiosInstance) {
 5 |   async function fetchAll(workspaceId: string) {
 6 |     return api.get(`workspaces/${workspaceId}/projects?archived=false`);
 7 |   }
 8 | 
 9 |   return { fetchAll };
10 | }
11 | 
12 | export const projectsService = ProjectsService(api);
13 | 
```

--------------------------------------------------------------------------------
/src/validation/entries/edit-entry-schema.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | 
 3 | export const EditEntrySchema = z.object({
 4 |   workspaceId: z.string(),
 5 |   timeEntryId: z.string(),
 6 |   billable: z.boolean().optional(),
 7 |   description: z.string().optional(),
 8 |   start: z.union([z.coerce.date(), z.undefined()]).optional(),
 9 |   end: z.union([z.coerce.date(), z.undefined()]).optional(),
10 |   projectId: z.string().optional(),
11 | });
12 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "clockify-mcp-server",
 3 |   "version": "1.1.1",
 4 |   "main": "index.js",
 5 |   "module": "./src/index.ts",
 6 |   "type": "module",
 7 |   "files": [
 8 |     "dist"
 9 |   ],
10 |   "scripts": {
11 |     "dev": "npx @smithery/cli dev",
12 |     "build": "npx @smithery/cli build"
13 |   },
14 |   "license": "MIT",
15 |   "engines": {
16 |     "node": ">=20.0.0"
17 |   },
18 |   "devDependencies": {
19 |     "@smithery/cli": "^1.2.4",
20 |     "@types/node": "^20.19.7",
21 |     "ts-node-dev": "^2.0.0",
22 |     "tsx": "^4.20.3",
23 |     "typescript": "^5.8.3"
24 |   },
25 |   "dependencies": {
26 |     "@modelcontextprotocol/sdk": "^1.12.1",
27 |     "axios": "^1.8.4",
28 |     "dotenv": "^16.5.0",
29 |     "zod": "^3.24.2"
30 |   }
31 | }
32 | 
```

--------------------------------------------------------------------------------
/test/users.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { after, describe, it } from "node:test";
 2 | import { createMcpClient, TEST_WORKSPACE_ID, TEST_USER_ID } from "./setup";
 3 | import assert from "node:assert";
 4 | import { ClockifyUser, McpResponse } from "../src/types";
 5 | 
 6 | describe("Users MCP Tests", async () => {
 7 |   const client = await createMcpClient();
 8 | 
 9 |   after(async () => {
10 |     await client.close();
11 |   });
12 | 
13 |   it("Retrieve current user info", async () => {
14 |     const response = (await client.callTool({
15 |       name: "get-current-user",
16 |     })) as McpResponse;
17 | 
18 |     const user: ClockifyUser = JSON.parse(response.content[0].text as string);
19 |     assert(user.id === TEST_USER_ID);
20 |   });
21 | });
22 | 
```

--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import dotenv from "dotenv";
 2 | dotenv.config();
 3 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 4 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 5 | 
 6 | export const TEST_WORKSPACE_ID = process.env.TEST_WORKSPACE_ID;
 7 | export const TEST_USER_ID = process.env.TEST_USER_ID;
 8 | export const TEST_PROJECT_ID = process.env.TEST_PROJECT_ID;
 9 | 
10 | export async function createMcpClient() {
11 |   const transport = new StdioClientTransport({
12 |     command: "ts-node",
13 |     args: ["src/index.ts"],
14 |   });
15 | 
16 |   const client = new Client({
17 |     name: "clockify-test-mcp-client",
18 |     version: "1.1.1",
19 |   });
20 | 
21 |   await client.connect(transport);
22 | 
23 |   return client;
24 | }
25 | 
```

--------------------------------------------------------------------------------
/src/tools/users.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { TOOLS_CONFIG } from "../config/api";
 2 | import { usersService } from "../clockify-sdk/users";
 3 | import {
 4 |   ClockifyUser,
 5 |   McpResponse,
 6 |   McpToolConfigWithoutParameters,
 7 | } from "../types";
 8 | 
 9 | export const getCurrentUserTool: McpToolConfigWithoutParameters = {
10 |   name: TOOLS_CONFIG.users.current.name,
11 |   description: TOOLS_CONFIG.users.current.description,
12 |   handler: async (): Promise<McpResponse> => {
13 |     const response = await usersService.getCurrent();
14 | 
15 |     const user: ClockifyUser = {
16 |       id: response.data.id,
17 |       name: response.data.name,
18 |       email: response.data.email,
19 |     };
20 | 
21 |     return {
22 |       content: [
23 |         {
24 |           type: "text",
25 |           text: JSON.stringify(user),
26 |         },
27 |       ],
28 |     };
29 |   },
30 | };
31 | 
```

--------------------------------------------------------------------------------
/src/tools/workspaces.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { TOOLS_CONFIG } from "../config/api";
 2 | import { workspacesService } from "../clockify-sdk/workspaces";
 3 | import {
 4 |   ClockifyWorkspace,
 5 |   McpResponse,
 6 |   McpToolConfigWithoutParameters,
 7 | } from "../types";
 8 | 
 9 | export const findWorkspacesTool: McpToolConfigWithoutParameters = {
10 |   name: TOOLS_CONFIG.workspaces.list.name,
11 |   description: TOOLS_CONFIG.workspaces.list.description,
12 |   handler: async (): Promise<McpResponse> => {
13 |     const response = await workspacesService.fetchAll();
14 | 
15 |     const workspaces = response.data.map((workspace: ClockifyWorkspace) => ({
16 |       name: workspace.name,
17 |       id: workspace.id,
18 |     }));
19 | 
20 |     return {
21 |       content: [
22 |         {
23 |           type: "text",
24 |           text: JSON.stringify(workspaces),
25 |         },
26 |       ],
27 |     };
28 |   },
29 | };
30 | 
```

--------------------------------------------------------------------------------
/test/workspaces.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { after, describe, it } from "node:test";
 2 | import assert from "node:assert";
 3 | import { createMcpClient, TEST_WORKSPACE_ID } from "./setup";
 4 | import { ClockifyWorkspace, McpResponse } from "../src/types";
 5 | 
 6 | describe("Workspaces MCP Tests", async () => {
 7 |   const client = await createMcpClient();
 8 | 
 9 |   after(async () => {
10 |     await client.close();
11 |   });
12 | 
13 |   it("should list all user workspaces", async () => {
14 |     const result = (await client.callTool({
15 |       name: "get-workspaces",
16 |     })) as McpResponse;
17 | 
18 |     const workspaces: ClockifyWorkspace[] = JSON.parse(
19 |       result.content[0].text as string
20 |     );
21 | 
22 |     assert(workspaces.length > 0);
23 |     assert(
24 |       workspaces.find(
25 |         (workspace: ClockifyWorkspace) => workspace.id === TEST_WORKSPACE_ID
26 |       )
27 |     );
28 |   });
29 | });
30 | 
```

--------------------------------------------------------------------------------
/src/tools/projects.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { projectsService } from "../clockify-sdk/projects";
 2 | import { TOOLS_CONFIG } from "../config/api";
 3 | import { z } from "zod";
 4 | import { McpResponse, McpToolConfig, TFindProjectSchema } from "../types";
 5 | 
 6 | export const findProjectTool: McpToolConfig = {
 7 |   name: TOOLS_CONFIG.projects.list.name,
 8 |   description: TOOLS_CONFIG.projects.list.description,
 9 |   parameters: {
10 |     workspaceId: z
11 |       .string()
12 |       .describe(
13 |         "The ID of the workspace that you need to get the projects from"
14 |       ),
15 |   },
16 |   handler: async ({
17 |     workspaceId,
18 |   }: TFindProjectSchema): Promise<McpResponse> => {
19 |     if (!workspaceId && typeof workspaceId === "string")
20 |       throw new Error("Workspace ID required to fetch projects");
21 | 
22 |     const response = await projectsService.fetchAll(workspaceId as string);
23 |     const projects = response.data.map((project: any) => ({
24 |       name: project.name,
25 |       clientName: project.clientName,
26 |       id: project.id,
27 |     }));
28 | 
29 |     return {
30 |       content: [
31 |         {
32 |           type: "text",
33 |           text: JSON.stringify(projects),
34 |         },
35 |       ],
36 |     };
37 |   },
38 | };
39 | 
```

--------------------------------------------------------------------------------
/src/config/api.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import axios from "axios";
 2 | 
 3 | export const api = axios.create({
 4 |   baseURL: process.env.CLOCKIFY_API_URL || 'https://api.clockify.me/api/v1',
 5 |   headers: {
 6 |     "X-Api-Key": `${process.env.CLOCKIFY_API_TOKEN}`,
 7 |   },
 8 | });
 9 | 
10 | export const SERVER_CONFIG = {
11 |   name: "Clockify MCP Server",
12 |   version: "1.0.0",
13 |   description:
14 |     "A service that integrates with Clockify API to manage time entries",
15 | };
16 | 
17 | export const TOOLS_CONFIG = {
18 |   workspaces: {
19 |     list: {
20 |       name: "get-workspaces",
21 |       description:
22 |         "Get user available workspaces id and name, a workspace is required to manage time entries",
23 |     },
24 |   },
25 |   projects: {
26 |     list: {
27 |       name: "get-projects",
28 |       description:
29 |         "Get workspace projects id and name, the projects can be associated with time entries",
30 |     },
31 |   },
32 |   users: {
33 |     current: {
34 |       name: "get-current-user",
35 |       description:
36 |         "Get the current user id and name, to search for entries is required to have the user id",
37 |     },
38 |   },
39 |   entries: {
40 |     create: {
41 |       name: "create-time-entry",
42 |       description:
43 |         "Register a new time entry of a task or break in a workspace",
44 |     },
45 |     list: {
46 |       name: "list-time-entries",
47 |       description: "Get registered time entries from a workspace",
48 |     },
49 |     delete: {
50 |       name: "delete-time-entry",
51 |       description: "Delete a specific time entry from a workspace",
52 |     },
53 |     edit: {
54 |       name: "edit-time-entry",
55 |       description: "Edit an existing time entry in a workspace",
56 |     },
57 |   },
58 | };
59 | 
```

--------------------------------------------------------------------------------
/src/clockify-sdk/entries.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { AxiosInstance } from "axios";
 2 | import { api } from "../config/api";
 3 | import {
 4 |   TCreateEntrySchema,
 5 |   TFindEntrySchema,
 6 |   TDeleteEntrySchema,
 7 |   TEditEntrySchema,
 8 | } from "../types";
 9 | import { URLSearchParams } from "node:url";
10 | 
11 | function EntriesService(api: AxiosInstance) {
12 |   async function create(entry: TCreateEntrySchema) {
13 |     const body = {
14 |       ...entry,
15 |       workspaceId: undefined,
16 |     };
17 | 
18 |     return api.post(`workspaces/${entry.workspaceId}/time-entries`, body);
19 |   }
20 | 
21 |   async function find(filters: TFindEntrySchema) {
22 |     const searchParams = new URLSearchParams();
23 | 
24 |     if (filters.description)
25 |       searchParams.append("description", filters.description);
26 | 
27 |     if (filters.start)
28 |       searchParams.append("start", filters.start.toISOString());
29 | 
30 |     if (filters.end) searchParams.append("end", filters.end.toISOString());
31 | 
32 |     if (filters.project) searchParams.append("project", filters.project);
33 | 
34 |     return api.get(
35 |       `https://api.clockify.me/api/v1/workspaces/${filters.workspaceId}/user/${
36 |         filters.userId
37 |       }/time-entries?${searchParams.toString()}`
38 |     );
39 |   }
40 | 
41 |   async function deleteEntry(params: TDeleteEntrySchema) {
42 |     return api.delete(
43 |       `workspaces/${params.workspaceId}/time-entries/${params.timeEntryId}`
44 |     );
45 |   }
46 | 
47 |   async function update(params: TEditEntrySchema) {
48 |     const body = {
49 |       ...params,
50 |       workspaceId: undefined,
51 |       timeEntryId: undefined,
52 |     };
53 | 
54 |     return api.put(
55 |       `workspaces/${params.workspaceId}/time-entries/${params.timeEntryId}`,
56 |       body
57 |     );
58 |   }
59 | 
60 |   async function getById(workspaceId: string, timeEntryId: string) {
61 |     return api.get(`workspaces/${workspaceId}/time-entries/${timeEntryId}`);
62 |   }
63 | 
64 |   return { create, find, deleteEntry, update, getById };
65 | }
66 | 
67 | export const entriesService = EntriesService(api);
68 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import dotenv from "dotenv";
 2 | dotenv.config();
 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 5 | import { api, SERVER_CONFIG } from "./config/api";
 6 | import {
 7 |   createEntryTool,
 8 |   deleteEntryTool,
 9 |   editEntryTool,
10 |   listEntriesTool,
11 | } from "./tools/entries";
12 | import { findProjectTool } from "./tools/projects";
13 | import { getCurrentUserTool } from "./tools/users";
14 | import { findWorkspacesTool } from "./tools/workspaces";
15 | import { z } from "zod";
16 | import { argv } from "process";
17 | 
18 | export const configSchema = z.object({
19 |   clockifyApiToken: z.string().describe("Clockify API Token"),
20 | });
21 | 
22 | const server = new McpServer(SERVER_CONFIG);
23 | 
24 | export default function createStatelessServer({
25 |   config,
26 | }: {
27 |   config: z.infer<typeof configSchema>;
28 | }) {
29 |   api.defaults.headers.Authorization = `Bearer ${config.clockifyApiToken}`;
30 |   server.tool(
31 |     createEntryTool.name,
32 |     createEntryTool.description,
33 |     createEntryTool.parameters,
34 |     createEntryTool.handler
35 |   );
36 | 
37 |   server.tool(
38 |     findProjectTool.name,
39 |     findProjectTool.description,
40 |     findProjectTool.parameters,
41 |     findProjectTool.handler
42 |   );
43 | 
44 |   server.tool(
45 |     listEntriesTool.name,
46 |     listEntriesTool.description,
47 |     listEntriesTool.parameters,
48 |     listEntriesTool.handler
49 |   );
50 | 
51 |   server.tool(
52 |     getCurrentUserTool.name,
53 |     getCurrentUserTool.description,
54 |     getCurrentUserTool.handler
55 |   );
56 | 
57 |   server.tool(
58 |     findWorkspacesTool.name,
59 |     findWorkspacesTool.description,
60 |     findWorkspacesTool.handler
61 |   );
62 | 
63 |   server.tool(
64 |     deleteEntryTool.name,
65 |     deleteEntryTool.description,
66 |     deleteEntryTool.parameters,
67 |     deleteEntryTool.handler
68 |   );
69 | 
70 |   server.tool(
71 |     editEntryTool.name,
72 |     editEntryTool.description,
73 |     editEntryTool.parameters,
74 |     editEntryTool.handler
75 |   );
76 |   return server.server;
77 | }
78 | 
79 | (() => {
80 |   if (argv.find((flag) => flag === "--local")) {
81 |     createStatelessServer({
82 |       config: {
83 |         clockifyApiToken: process.env.CLOCKIFY_API_TOKEN as string,
84 |       },
85 |     });
86 |     const transport = new StdioServerTransport();
87 |     server.connect(transport);
88 |   }
89 | })();
90 | 
```

--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | import { CreateEntrySchema } from "../validation/entries/create-entry-schema";
 3 | import { FindEntrySchema } from "../validation/entries/find-entry-schema";
 4 | import { DeleteEntrySchema } from "../validation/entries/delete-entry-schema";
 5 | import { EditEntrySchema } from "../validation/entries/edit-entry-schema";
 6 | import {
 7 |   ReadResourceTemplateCallback,
 8 |   ResourceMetadata,
 9 |   ResourceTemplate,
10 | } from "@modelcontextprotocol/sdk/server/mcp";
11 | import { FindProjectSchema } from "../validation/projects/find-project-schema";
12 | 
13 | export type TCreateEntrySchema = z.infer<typeof CreateEntrySchema>;
14 | 
15 | export type TFindEntrySchema = z.infer<typeof FindEntrySchema>;
16 | 
17 | export type TDeleteEntrySchema = z.infer<typeof DeleteEntrySchema>;
18 | 
19 | export type TEditEntrySchema = z.infer<typeof EditEntrySchema>;
20 | 
21 | export type TFindProjectSchema = z.infer<typeof FindProjectSchema>;
22 | 
23 | export interface ClockifyWorkspace {
24 |   id: string;
25 |   name: string;
26 | }
27 | 
28 | export interface ClockifyUser {
29 |   id: string;
30 |   name: string;
31 |   email: string;
32 | }
33 | 
34 | export interface McpToolConfig {
35 |   name: string;
36 |   description: string;
37 |   parameters: Record<string, any>;
38 |   handler: (params: any) => Promise<McpResponse>;
39 | }
40 | 
41 | export type McpToolConfigWithoutParameters = Omit<McpToolConfig, "parameters">;
42 | 
43 | export interface McpTextContent {
44 |   type: "text";
45 |   text: string;
46 |   [key: string]: unknown;
47 | }
48 | 
49 | export interface McpImageContent {
50 |   type: "image";
51 |   data: string;
52 |   mimeType: string;
53 |   [key: string]: unknown;
54 | }
55 | 
56 | export interface McpResourceConfig {
57 |   name: string;
58 |   template: ResourceTemplate;
59 |   metadata: ResourceMetadata;
60 |   handler: ReadResourceTemplateCallback;
61 | }
62 | 
63 | export interface McpResourceContent {
64 |   type: "resource";
65 |   resource:
66 |     | {
67 |         text: string;
68 |         uri: string;
69 |         mimeType?: string;
70 |         [key: string]: unknown;
71 |       }
72 |     | {
73 |         uri: string;
74 |         blob: string;
75 |         mimeType?: string;
76 |         [key: string]: unknown;
77 |       };
78 |   [key: string]: unknown;
79 | }
80 | 
81 | export type McpContent = McpTextContent | McpImageContent | McpResourceContent;
82 | 
83 | export interface McpResponse {
84 |   content: McpContent[];
85 |   _meta?: Record<string, unknown>;
86 |   isError?: boolean;
87 |   [key: string]: unknown;
88 | }
89 | 
```

--------------------------------------------------------------------------------
/test/entries.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { after, describe, it } from "node:test";
  2 | import { createMcpClient, TEST_WORKSPACE_ID, TEST_PROJECT_ID } from "./setup";
  3 | import { McpResponse } from "../src/types";
  4 | import assert from "node:assert";
  5 | 
  6 | let createdEntryId: string;
  7 | 
  8 | describe("Entries MCP Tests", async () => {
  9 |   const client = await createMcpClient();
 10 | 
 11 |   after(async () => {
 12 |     await client.close();
 13 |   });
 14 | 
 15 |   it("Create a billable time entry without project", async () => {
 16 |     const dateOneHourBefore = new Date();
 17 |     dateOneHourBefore.setHours(dateOneHourBefore.getHours() - 1);
 18 | 
 19 |     const currentDate = new Date();
 20 | 
 21 |     const response = (await client.callTool({
 22 |       name: "create-time-entry",
 23 |       arguments: {
 24 |         workspaceId: TEST_WORKSPACE_ID,
 25 |         billable: true,
 26 |         description: "MCP Test Entry",
 27 |         start: dateOneHourBefore,
 28 |         end: currentDate,
 29 |       },
 30 |     })) as McpResponse;
 31 | 
 32 |     assert(
 33 |       (response.content[0].text as string).startsWith(
 34 |         "Time entry created successfully"
 35 |       )
 36 |     );
 37 |   });
 38 | 
 39 |   it("Create a time entry with project", async () => {
 40 |     const dateTwoHoursBefore = new Date();
 41 |     dateTwoHoursBefore.setHours(dateTwoHoursBefore.getHours() - 2);
 42 | 
 43 |     const dateOneHourBefore = new Date();
 44 |     dateOneHourBefore.setHours(dateOneHourBefore.getHours() - 1);
 45 | 
 46 |     const response = (await client.callTool({
 47 |       name: "create-time-entry",
 48 |       arguments: {
 49 |         workspaceId: TEST_WORKSPACE_ID,
 50 |         billable: false,
 51 |         description: "MCP Test Entry with Project",
 52 |         start: dateTwoHoursBefore,
 53 |         end: dateOneHourBefore,
 54 |         projectId: TEST_PROJECT_ID,
 55 |       },
 56 |     })) as McpResponse;
 57 | 
 58 |     assert(
 59 |       (response.content[0].text as string).startsWith(
 60 |         "Time entry created successfully"
 61 |       )
 62 |     );
 63 | 
 64 |     const match = (response.content[0].text as string).match(/ID: ([^\s]+)/);
 65 |     if (match) {
 66 |       createdEntryId = match[1];
 67 |     }
 68 |   });
 69 | 
 70 |   it("Edit a time entry", async () => {
 71 |     if (!createdEntryId) {
 72 |       throw new Error("No entry ID available for editing");
 73 |     }
 74 | 
 75 |     const response = (await client.callTool({
 76 |       name: "edit-time-entry",
 77 |       arguments: {
 78 |         workspaceId: TEST_WORKSPACE_ID,
 79 |         timeEntryId: createdEntryId,
 80 |         description: "MCP Test Entry Edited",
 81 |         billable: false,
 82 |       },
 83 |     })) as McpResponse;
 84 |     assert(
 85 |       (response.content[0].text as string).startsWith(
 86 |         "Time entry updated successfully"
 87 |       )
 88 |     );
 89 |   });
 90 | 
 91 |   it("Delete a time entry", async () => {
 92 |     if (!createdEntryId) {
 93 |       throw new Error("No entry ID available for deletion");
 94 |     }
 95 | 
 96 |     const response = (await client.callTool({
 97 |       name: "delete-time-entry",
 98 |       arguments: {
 99 |         workspaceId: TEST_WORKSPACE_ID,
100 |         timeEntryId: createdEntryId,
101 |       },
102 |     })) as McpResponse;
103 | 
104 |     assert(
105 |       (response.content[0].text as string).startsWith("Time entry with ID")
106 |     );
107 |     assert(
108 |       (response.content[0].text as string).includes("was deleted successfully")
109 |     );
110 |   });
111 | });
112 | 
```

--------------------------------------------------------------------------------
/src/tools/entries.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import { TOOLS_CONFIG } from "../config/api";
  3 | import { entriesService } from "../clockify-sdk/entries";
  4 | import {
  5 |   McpResponse,
  6 |   McpToolConfig,
  7 |   TCreateEntrySchema,
  8 |   TFindEntrySchema,
  9 |   TDeleteEntrySchema,
 10 |   TEditEntrySchema,
 11 | } from "../types";
 12 | 
 13 | export const createEntryTool: McpToolConfig = {
 14 |   name: TOOLS_CONFIG.entries.create.name,
 15 |   description: TOOLS_CONFIG.entries.create.description,
 16 |   parameters: {
 17 |     workspaceId: z
 18 |       .string()
 19 |       .describe("The id of the workspace that gonna be saved the time entry"),
 20 |     billable: z
 21 |       .boolean()
 22 |       .describe("If the task is billable or not")
 23 |       .optional()
 24 |       .default(true),
 25 |     description: z.string().describe("The description of the time entry"),
 26 |     start: z.coerce.date().describe("The start of the time entry"),
 27 |     end: z.coerce.date().describe("The end of the time entry"),
 28 |     projectId: z
 29 |       .string()
 30 |       .optional()
 31 |       .describe("The id of the project associated with this time entry"),
 32 |   },
 33 |   handler: async (params: TCreateEntrySchema): Promise<McpResponse> => {
 34 |     try {
 35 |       const result = await entriesService.create(params);
 36 | 
 37 |       const entryInfo = `Time entry created successfully. ID: ${result.data.id} Name: ${result.data.description}`;
 38 | 
 39 |       return {
 40 |         content: [
 41 |           {
 42 |             type: "text",
 43 |             text: entryInfo,
 44 |           },
 45 |         ],
 46 |       };
 47 |     } catch (error: any) {
 48 |       throw new Error(`Failed to create entry: ${error.message}`);
 49 |     }
 50 |   },
 51 | };
 52 | 
 53 | export const listEntriesTool: McpToolConfig = {
 54 |   name: TOOLS_CONFIG.entries.list.name,
 55 |   description: TOOLS_CONFIG.entries.list.description,
 56 |   parameters: {
 57 |     workspaceId: z
 58 |       .string()
 59 |       .describe("The id of the workspace that gonna search for the entries"),
 60 |     userId: z
 61 |       .string()
 62 |       .describe(
 63 |         "The id of the user that gonna have the entries searched, default is the current user id"
 64 |       ),
 65 |     description: z
 66 |       .string()
 67 |       .optional()
 68 |       .describe("The time entry description to search for"),
 69 |     start: z.coerce
 70 |       .date()
 71 |       .optional()
 72 |       .describe("Start time of the entry to search for"),
 73 |     end: z.coerce
 74 |       .date()
 75 |       .optional()
 76 |       .describe("End time of the entry to search for"),
 77 |     project: z
 78 |       .string()
 79 |       .optional()
 80 |       .describe("The id of the project to search for entries"),
 81 |   },
 82 |   handler: async (params: TFindEntrySchema) => {
 83 |     try {
 84 |       const result = await entriesService.find(params);
 85 | 
 86 |       const formmatedResults = result.data.map((entry: any) => ({
 87 |         id: entry.id,
 88 |         description: entry.description,
 89 |         duration: entry.duration,
 90 |         start: entry.start,
 91 |         end: entry.end,
 92 |       }));
 93 | 
 94 |       return {
 95 |         content: [
 96 |           {
 97 |             type: "text",
 98 |             text: JSON.stringify(formmatedResults),
 99 |           },
100 |         ],
101 |       };
102 |     } catch (error: any) {
103 |       throw new Error(`Failed to retrieve entries: ${error.message}`);
104 |     }
105 |   },
106 | };
107 | 
108 | export const deleteEntryTool: McpToolConfig = {
109 |   name: TOOLS_CONFIG.entries.delete.name,
110 |   description: TOOLS_CONFIG.entries.delete.description,
111 |   parameters: {
112 |     workspaceId: z
113 |       .string()
114 |       .describe("The id of the workspace where the time entry is located"),
115 |     timeEntryId: z.string().describe("The id of the time entry to be deleted"),
116 |   },
117 |   handler: async (params: TDeleteEntrySchema): Promise<McpResponse> => {
118 |     try {
119 |       await entriesService.deleteEntry(params);
120 | 
121 |       return {
122 |         content: [
123 |           {
124 |             type: "text",
125 |             text: `Time entry with ID ${params.timeEntryId} was deleted successfully.`,
126 |           },
127 |         ],
128 |       };
129 |     } catch (error: any) {
130 |       throw new Error(`Failed to delete entry: ${error.message}`);
131 |     }
132 |   },
133 | };
134 | 
135 | export const editEntryTool: McpToolConfig = {
136 |   name: TOOLS_CONFIG.entries.edit.name,
137 |   description: TOOLS_CONFIG.entries.edit.description,
138 |   parameters: {
139 |     workspaceId: z
140 |       .string()
141 |       .describe("The id of the workspace where the time entry is located"),
142 |     timeEntryId: z.string().describe("The id of the time entry to be edited"),
143 |     billable: z.boolean().describe("If the task is billable or not").optional(),
144 |     description: z
145 |       .string()
146 |       .describe("The description of the time entry")
147 |       .optional(),
148 |     start: z.coerce.date().describe("The start of the time entry").optional(),
149 |     end: z.coerce.date().describe("The end of the time entry").optional(),
150 |     projectId: z
151 |       .string()
152 |       .optional()
153 |       .describe("The id of the project associated with this time entry"),
154 |   },
155 |   handler: async (params: TEditEntrySchema): Promise<McpResponse> => {
156 |     try {
157 |       let start = params.start;
158 |       if (!start) {
159 |         const current = await entriesService.getById(
160 |           params.workspaceId,
161 |           params.timeEntryId
162 |         );
163 |         start = new Date(current.data.timeInterval.start);
164 |       }
165 |       const result = await entriesService.update({
166 |         ...params,
167 |         start,
168 |       });
169 | 
170 |       const entryInfo = `Time entry updated successfully. ID: ${result.data.id} Name: ${result.data.description}`;
171 | 
172 |       return {
173 |         content: [
174 |           {
175 |             type: "text",
176 |             text: entryInfo,
177 |           },
178 |         ],
179 |       };
180 |     } catch (error: any) {
181 |       throw new Error(`Failed to edit entry: ${error.message}`);
182 |     }
183 |   },
184 | };
185 | 
```