@d4works/mcp-clockify
v0.10.0
Published
MCP server for Clockify time tracking
Downloads
870
Maintainers
Readme
MCP Clockify Server
MCP server for Clockify time tracking integration with Claude.
Features
- Start/Stop Timer - Track time on tasks identified by a reference (e.g., ABC-123)
- Configurable task naming -
[TMS-123] Popis,(#1234) Popis, plain names - per project - Task management - create, rename, mark done, delete
- One ticket, several report items - opt-in duplicate references, with ambiguity refused everywhere it could misattribute time
- Time entry editing - move an entry to another task, change times, tags or billable; delete
- Tags - list, filter listings by tag, attach on create, preserved on every edit
- Reading what is logged - per-day listings and range summaries with totals
- Time Multiplication - Duplicate time entries for billing adjustments
- Self-configuration - Guided setup wizard for workspace and project selection
Installation
1. Get your Clockify API key
2. Create or update .env file in your project
CLOCKIFY_API_KEY=your_api_key_here3. Add to your project .mcp.json
{
"mcpServers": {
"clockify": {
"command": "bash",
"args": [
"-c",
"export $(cat .claude/.env.local | xargs) && npx -y @d4works/mcp-clockify@latest"
]
}
}
}The source .env command loads your CLOCKIFY_API_KEY from the .env file before starting the server.
4. Restart Claude Code and configure
After restart, tell Claude to configure Clockify
The configuration wizard will guide you through selecting your workspace, project(s), task prefix, and language.
Configuration
Configuration lives in environment variables (typically loaded from .env.local). The API key is the only secret; project ids are not.
Single project (backward compatible)
CLOCKIFY_API_KEY=...
CLOCKIFY_WORKSPACE_ID=...
CLOCKIFY_PROJECT_ID=66c452f7e03e190db06ad8df
CLOCKIFY_TASK_PREFIX=TMS
CLOCKIFY_LANGUAGE=csThis existing format keeps working unchanged - it is loaded as a single project with the alias default.
Multiple projects
Define N projects via alias-based variables. The alias is the part of the name between CLOCKIFY_PROJECT_ and _ID. Each project needs an _ID (required); _TASK_PREFIX is optional.
CLOCKIFY_API_KEY=...
CLOCKIFY_WORKSPACE_ID=...
CLOCKIFY_LANGUAGE=cs
# Alias "vyvoj" - tasks named "[TMS-123] ..."
CLOCKIFY_PROJECT_VYVOJ_ID=66c452f7e03e190db06ad8df
CLOCKIFY_PROJECT_VYVOJ_TASK_PREFIX=TMS
# Alias "udrzba" - no prefix convention (tasks named plainly)
CLOCKIFY_PROJECT_UDRZBA_ID=68edd67b46d55c4e74f3261f
# Which project is used when a tool omits `project`:
CLOCKIFY_DEFAULT_PROJECT=vyvoj- Default project: with a single project it is the default automatically. With several projects,
CLOCKIFY_DEFAULT_PROJECTselects it; without it, tools that omitprojectreturn an error listing the available aliases. projectparameter: project-targeting tools (start-timer,add-time-entry,assign-task-to-running,create-task,get-task,update-task,delete-task,list-tasks) accept an optionalproject(alias or project id). Only configured projects are accepted (allowlist).- Task references:
123/TMS-123are just text inside the task name, not an identifier - several tasks may carry the same one (see Several tasks under one reference). How that text is written into the name is the task name format below. For projects that store plain names, reference a task by its internal Clockify id - list them withlist-tasks.
Task name format
How a reference and a description become the name Clockify stores. The template takes two
placeholders: {id} (the reference) and {name} (the description).
# Global default for every project
CLOCKIFY_TASK_NAME_FORMAT="(#{id}) {name}"
# ...or per project (wins over the global one)
CLOCKIFY_PROJECT_EFLIGHT_TASK_NAME_FORMAT="(#{id}) {name}"
CLOCKIFY_PROJECT_VYVOJ_TASK_NAME_FORMAT="[{id}] {name}"| Template | create-task 2767 "Popis" produces | Lookup searches for |
| ----------------- | ----------------------------------- | ------------------- |
| [{id}] {name} | [TMS-2767] Popis (default) | [TMS-2767] |
| (#{id}) {name} | (#2767) Popis | (#2767) |
| {name} (#{id}) | Popis (#2767) | (#2767) |
| {name} | Popis - no reference stored | nothing (see below) |
- The same template drives creation and lookup. A task created as
(#2767) Popisis found by searching for(#2767)- creating under one convention and searching under another is how a server ends up unable to find the tasks it made, and then creates them again as duplicates. - Backward compatible. Without any
*_TASK_NAME_FORMATthe default[{id}] {name}applies, so existing prefix-based projects are unchanged. {name}-only projects store plain names, so no reference can be matched in them: address their tasks by internal id (list-tasks), andcreate-taskrefuses because it cannot check for duplicates.- A rejected template (no
{name}, an unknown placeholder, a repeated one) does not stop the server: the default is used,list-configured-projectsshows the error, andcreate-task/update-taskrefuse rather than storing a name built from a template you did not configure.
Regional workspaces
CLOCKIFY_API_BASE_URL=https://euc1.api.clockify.me/api/v1 # optionalClockify serves some workspaces from regional hosts. Omit this for the global API.
Billable
New time entries are created billable by default. Clockify's API does not inherit a project's "billable by default" setting when an entry is created over the API - an omitted flag yields a non-billable entry (a documented Clockify wontfix), so this server sends billable explicitly.
CLOCKIFY_BILLABLE=false # optional; default is trueSet CLOCKIFY_BILLABLE to false / 0 / no / off to make new entries non-billable instead. The setting is global (not per-project). Editing an existing entry (e.g. changing a running timer's task or a stopped entry's description) preserves whatever billable status the entry already had.
Available Tools
| Tool | Description |
| --------------------------- | ---------------------------------------------------- |
| test-connection | Test Clockify API connection |
| list-configured-projects | List configured projects (aliases, ids, prefixes, name format) |
| list-tasks | List a project's tasks with their internal ids |
| list-tags | List the workspace's tags with their internal ids |
| list-time-entries | List entries already logged, with totals per day/task |
| get-time-summary | Aggregate logged time over a range, without entries |
| get-running-timer | Check if a timer is running |
| start-timer | Start timer for a task (optionally with tags) |
| stop-timer | Stop running timer |
| multiply-time | Duplicate last time entry |
| add-time-entry | Add a retroactive time entry (optionally with tags) |
| update-time-entry | Edit an entry: move to another task, times, tags, billable |
| delete-time-entry | Delete one time entry (irreversible) |
| create-task | Create new task in Clockify (allowDuplicateRef for a second task under one reference) |
| get-task | Get task info by reference or internal id |
| update-task | Rename a task or mark it ACTIVE / DONE |
| delete-task | Delete a task (irreversible; its entries lose their task) |
| assign-task-to-running | Assign or change task on a running timer |
| reconfigure | Change workspace/project settings |
Several tasks under one reference
One ticket often covers several pieces of work that the client must see as separate lines of the
report - the report shows task names, not descriptions. A three-hour crash investigation inside
PIZZA-401 Redesign app is its own item, but it is still PIZZA-401.
By default create-task refuses a reference that already has a task: that guard is what stops a
failed lookup from quietly becoming a duplicate. allowDuplicateRef: true lifts it on purpose:
Create task PIZZA-401 "Oprava pádu při startu" even though PIZZA-401 already existsWhat follows from that, and is enforced rather than merely documented:
- The reply lists every task now carrying the reference, with its internal id, so the follow-up call can name the right one.
- The reference then addresses nothing on its own.
start-timer,add-time-entry,assign-task-to-running,get-task,update-task,delete-taskandupdate-time-entryall answerAMBIGUOUS: N tasks ... match the reference, print the candidates and ask for the internal 24-char hex id. None of them picks the first match - a silent guess here means tracked time on the wrong task, discovered at invoicing. The collapse from "matching tasks" to "the task to write to" happens in exactly one function (resolveSingleTaskinsrc/tasks.ts), so no tool can skip the check. get-time-summaryis the exception, deliberately. It does not refuse an ambiguoustaskfilter: it includes every matching task and reports each separately in the by-task breakdown, with a warning that the headline total is their sum and belongs to no single task. Pass an internal id to get one task on its own.allowDuplicateRefdoes not override a truncated search. If the project's task list was cut short, creation is refused even with the flag - "add a task next to the ones listed" says nothing about tasks nobody has seen.- The default is unchanged, so existing setups behave exactly as before.
Editing what is already logged
Clockify's API has no partial update: a PUT replaces the whole object, so any field left out
is cleared - an omitted billable turns non-billable, omitted tagIds empties the tags, and an
omitted end turns a finished entry back into a running timer. Every edit here is therefore a
read-modify-write, and the result is verified against Clockify's own response.
update-time-entryis a partial edit. Only the parameters you pass change. The date, the start/end, the duration, the description, the tags and the billable flag are preserved, and the tool re-reads the stored entry afterwards and reports any field that moved anyway.- Moving an entry between tasks does not change any total. The same minutes stay on the same day; only the task attribution changes. That is what makes "split this 66-hour task into four phase tasks" safe: monthly sums are provably unaffected.
- Entry ids come from
list-time-entries- every entry line ends withid: `<id>`. - Locked, approved, HOLIDAY and TIME_OFF entries are refused rather than edited, because a
PUTcould not round-trip them faithfully. update-taskrenames without touching time. The task keeps its id, so every time entry stays attached; assignees, estimate, billable and status are written back unchanged and verified. Marking a taskDONEis what Clockify calls archiving - there is no separate archive.- Deleting a task does not delete its time. The entries stay on the project and lose their task,
irreversibly.
delete-tasksays how much time that was.
Tags
What did I log yesterday with the tag "moved"?
Tag entry <id> as moved
Show me this month's entries tagged movedlist-time-entriesandget-time-summaryshow each entry's tags and take an optionaltagfilter (a tag name or id). A filtered view says it is partial and how much it removed.- An unknown tag name is an error listing the available tags - never an empty result, which would read as "nothing is logged".
start-timerandadd-time-entryaccepttags;update-time-entryacceptstags(replace),addTagsandremoveTags.- Tags survive every edit. Stopping a timer with a new description, assigning a task to a
running timer and
multiply-timeall preserve them - which they did not before, because thePUTthat omitstagIdsempties them silently.
Reading what is already logged
list-time-entries and get-time-summary answer the question the writing tools cannot: what is
already in Clockify? Use list-time-entries before adding an entry to see whether that work is
already recorded.
- Your own entries only. Both tools read the per-user endpoint, so they show the entries of the API key's owner. They never show other users, a team, or the whole workspace.
- Days are local.
date/from/toare calendar days in the server's local timezone, converted to the UTC instants the API wants.tois inclusive. - An entry belongs to the day it started. One that runs from 22:40 to 00:20 is counted once, in full, on the day it began - the same grouping the Clockify web report uses.
- A running timer is shown separately and is never counted in any total. The output also states what the total would be if it were stopped now.
- All projects by default. Omitting
projectcovers everything, including projects that are not configured on this server (labelled(not configured)) - otherwise time tracked elsewhere would look like unlogged work. Passingprojectnarrows the view and the output says it is partial. - A truncated fetch is reported loudly. If the page ceiling is reached, every total becomes an explicit lower bound and the call is returned as an error rather than as a plausible number.
- Ranges are capped at 366 days; a longer range is refused instead of silently truncated.
What did I track today?
Is task 2923 already logged for yesterday?
How many hours are on 2812 this month?Smart Timer Handling
When you try to start a timer while another is already running, the server will prompt you with options:
If the running timer has a task:
- Stop running timer and start new one
- Keep running timer
If the running timer has no task assigned:
- Assign task to running timer (keeps the current time)
- Stop running timer and start new one
- Keep running timer
No changes are made without your explicit confirmation.
Usage Examples
Start tracking:
Start timer for task 123 with description "Implementing login form"Stop timer:
Stop the timerCheck status:
What timer is currently running?Multiple projects:
Start timer for task 123 in the udrzba project with description "..."List tasks in the udrzba projectIf a project stores plain task names, reference the task by the internal id shown by list-tasks.
Fix a badly named task:
Rename task 2767 to "Import letových plánů"Split one task's hours across phase tasks (no total changes):
List my entries for July on task 2767
Move entry <id> to task 2768
Tag entry <id> as movedDevelopment
Tests
pnpm test # unit tests (node:test) over the pure modules
pnpm test:e2e # drives the built server over MCP stdio against a fake Clockify APIpnpm test:e2e covers what unit tests cannot: that an ambiguous reference really is
refused by every writing tool, and that nothing was written when it was. VERBOSE=1
prints every tool response.
Publishing a new release
# Login to npm (if not already logged in)
npm login
# Push changes and tag
git push && git push --tags
# Verify package contents
npm publish --dry-run
# Publish to npm
npm publish --access publicLicense
MIT
