clockify-mcp
# 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
Scored across 8 tools
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.
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.
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.
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.