# 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 | [](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 |
```