Skip to main content
Glama
duynhannguyen

clockify-mcp

README.md
# clockify-mcp

A local [MCP](https://modelcontextprotocol.io) server that lets Claude start,
stop, and log Clockify time entries.

It runs as a child process over stdio — no port, nothing listening, no inbound
network surface. Claude spawns it, they exchange JSON-RPC over stdin/stdout, and
the server talks to `api.clockify.me` over HTTPS on your behalf.

```
Claude Code ──JSON-RPC/stdio──► clockify-mcp ──HTTPS + x-api-key──► Clockify
```

## Setup

**1. Get a Clockify API key.** In the Clockify web app: **Preferences →
Advanced → API → generate**. It's a static personal key — no OAuth, no expiry —
and it carries exactly your own permissions.

**2. Configure.**

```sh
cp .env.example .env
# then edit .env and paste the key after CLOCKIFY_API_KEY=
```

`.env` is gitignored. The server reads it from its own directory, so the key
stays out of `~/.claude.json`.

`CLOCKIFY_WORKSPACE_ID` is optional — the server reads your active workspace
from `GET /v1/user` on first use. Only set it if you belong to several
workspaces and want to pin one.

**3. Build.**

```sh
npm install
npm run build
```

**4. Register with Claude Code** (user scope makes it available in every
project):

```sh
claude mcp add clockify --scope user -- node D:/UniqProject/clockify-mcp/dist/index.js
claude mcp list          # expect: clockify: ... ✔ Connected
```

If you would rather keep the key in Claude's config than in `.env`, add
`-e CLOCKIFY_API_KEY=<key>` before the `--`.

## Tools

| Tool | Arguments | What it does |
|---|---|---|
| `list_projects` | `name?`, `includeArchived?` | Find a `projectId`. Active projects only by default. |
| `list_tasks` | `projectId`, `name?` | Find a `taskId`. Active tasks only. |
| `list_tags` | — | Find `tagIds`. |
| `current_timer` | — | What's running, and for how long. |
| `start_timer` | `description`, `projectId?`, `taskId?`, `tagIds?`, `billable?`, `start?` | Start a timer. |
| `stop_timer` | `end?` | Stop the running timer, report the duration. |
| `log_time` | `description`, `start`, `end`, `projectId?`, `taskId?`, `tagIds?`, `billable?` | Back-fill a finished entry. |
| `list_time_entries` | `start?`, `end?`, `projectId?`, `pageSize?` | Entries for a period with a total. Defaults to today. |

In practice you don't call these by name — you say *"clock 2 hours on the
launcher bug"* and Claude looks up the project, then logs it.

### Times

Every time argument accepts:

| You write | Interpreted as |
|---|---|
| `2026-08-01T09:30` | 09:30 **local** time |
| `2026-08-01 09:30` | same |
| `2026-08-01T09:30:00Z` | 09:30 UTC |
| `2026-08-01T09:30:00+07:00` | as written |
| `2026-08-01` | local midnight |

Output is shown in **your Clockify account's timezone** (read from your profile,
e.g. `Asia/Saigon`), so it matches what the Clockify web UI shows. Clockify
stores UTC; the conversion happens here.

### Gotcha: the list endpoint filters by wall clock, not UTC

`GET /v1/workspaces/{ws}/user/{id}/time-entries` takes `start`/`end` in a format
ending in `Z`, but it does **not** compare them as UTC — it compares them
against wall-clock time in the account's timezone. Verified against the live API
on an `Asia/Saigon` (UTC+7) account:

| `start` / `end` sent | Result |
|---|---|
| `2026-07-31T17:00:00Z` → `2026-08-01T16:59:59Z`<br>(honest UTC for local Aug 1) | returns **Jul 31's** entries |
| `2026-08-01T00:00:00Z` → `2026-08-01T23:59:59Z`<br>(local wall clock, `Z` kept) | returns **Aug 1's** entries ✓ |

Entry timestamps in POST/PUT **bodies** are ordinary UTC — the quirk is
filter-only, which makes it easy to miss. `toZoneFilter` in `src/time.ts` is the
one place that compensates; don't "fix" it back to UTC.

### Notes

- Clockify permits only one running timer. `start_timer` therefore stops any
  timer already running — the tool reports this explicitly rather than letting
  it happen silently.
- `stop_timer` with nothing running answers "No timer was running." rather than
  erroring.
- There is deliberately **no** update or delete tool, so the server cannot
  modify or destroy existing entries. The worst case is a spurious new entry you
  can remove in the web UI.

## Development

```sh
npm run watch    # tsc --watch
```

Smoke-test the protocol without involving Claude:

```sh
printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | node dist/index.js
```

Only protocol messages go to stdout — anything else would corrupt the stream, so
logs go to stderr.

## Security

The API key is a bearer credential with full access to your Clockify time data.
It sits in plaintext in `.env`, readable by anything running as your user; there
is no OS keychain integration here.

The key never leaves your machine — it goes into an outbound header and is not
sent to Anthropic. Tool *results* (project names, entry descriptions, durations)
do become part of the conversation, so they reach the model like any other tool
output.

## Layout

```
src/time.ts       local <-> UTC conversion, ISO-8601 durations
src/clockify.ts   HTTP client, error mapping, cached identity lookup
src/tools.ts      the 8 tool registrations
src/index.ts      env loading, McpServer, stdio transport
```

Built on `@modelcontextprotocol/server` v2. Requires Node >= 20.

TDQS

A4.3/5.0

Scored across 8 tools

Disambiguation5/5

Each tool targets a distinct resource or action: live timer controls, historical entry listing/creation, and reference lookups. start_timer and log_time are clearly separated by live vs. past work, and current_timer is clearly distinct from list_time_entries.

Naming Consistency4/5

Seven of eight tools follow a consistent verb_noun pattern (start_timer, stop_timer, log_time, list_*). current_timer is the only outlier, using a noun phrase instead of a verb-led name.

Tool Count5/5

Eight tools is well-scoped for a focused Clockify time-tracking server. Each tool earns its place: timer state, entry creation/listing, and ID lookups for projects, tasks, and tags.

Completeness3/5

The core timer loop and time listing are well covered, but updating or deleting time entries is missing. An agent cannot correct or remove a mistakenly logged entry, which is a notable workflow dead end.

Maintenance

ActivityMaintained
ResponsivenessNo issues