mcp-time-tracker
MCP server for time tracking: timesheets, a timesheet and a billable hours tracker. Track billable time without leaving the chat.
Open source Open in the app JSON README (API)
About
MCP server for time tracking: timesheets, a timesheet and a billable hours tracker. Track billable time without leaving the chat.
Details
- Kind
- Plugins
- Topic
- No topic detected
- Publisher
- theluckystrike
- Origin
- gemini
- Category
- ferramentas
- Version
- 0.21.0
- Last push
- 2026-09-10T14:23:54Z
- Repository state
- ativo
- Language
- JavaScript
- License
- MIT
- Added
- 2026-09-10 20:03:17
- Updated
- 2026-09-10 20:03:17
- Origin id
theluckystrike/mcp-time-tracker
README
# mcp-time-tracker
<!-- mirror-seo:start -->
**MCP server for time tracking: timesheets, a timesheet and a billable hours tracker.** Track billable time without leaving the chat.
Works with Claude Desktop, Claude Code, Cursor and any Model Context Protocol client. Runs on your own machine, or hosted with no install.
## Install
**Hosted, nothing to install.** Get a token from <https://mcp.zovo.one/mcp/connect> (the connect page) or <https://mcp.zovo.one/mcp/token> (the same token as JSON); a free anonymous one is issued on the spot and a Pro key works the same way. Then point an MCP client at `https://mcp.zovo.one/mcp/time-tracker` over streamable-http and send the token as `Authorization: Bearer <token>`.
If your client cannot set headers, put the token in the path instead: `https://mcp.zovo.one/mcp/time-tracker/t/<token>`. Both forms work. The bare URL with no token answers 401 on `tools/call`, so the token is not optional.
**Claude Desktop, one click.** Download `time-tracker.mcpb` from the [latest release](https://github.com/theluckystrike/mcp-servers/releases/latest) and double-click it.
**From source.** The mirror is self-contained: every `@theluckystrike/*` dependency is vendored, so a fresh clone builds with no extra setup.
```sh
git clone https://github.com/theluckystrike/mcp-time-tracker.git
cd mcp-time-tracker
npm install && npm run build
```
Then point your client at the built entry point:
```json
{
"mcpServers": {
"time-tracker": {
"command": "node",
"args": ["/absolute/path/to/mcp-time-tracker/dist/index.js"]
}
}
}
```
> `@theluckystrike/mcp-time-tracker` is **not published on npm yet**, so an `npx -y @theluckystrike/mcp-time-tracker` command will fail. The three paths above are the working ones and each is exercised by CI.

Read-only mirror of [mcp-servers/servers/time-tracker](https://github.com/theluckystrike/mcp-servers/tree/main/servers/time-tracker). See [MIRROR.md](MIRROR.md).
<!-- mirror-seo:end -->
Track billable time without leaving your AI chat. Say "start a timer on the acme redesign", keep working, then
ask for "my hours this week by project" or "invoice lines for acme in August". It keeps a running timer, lets you
log time you forgot to track, applies your hourly rate per project, and turns the result into a report, a CSV file
or a set of invoice line items. Everything is stored as plain JSON on your own machine.
Built by [theluckystrike](https://github.com/theluckystrike).
**Track billable time from chat and turn it straight into a report or invoice line items -- zero setup, all local.**
## 60-second install
npm publish for `@theluckystrike/mcp-time-tracker` is pending. Until then, the `.mcpb` one-click bundle or a clone+build
is the working path -- both are verified below.
**One-click (.mcpb):** download `time-tracker.mcpb` from the latest release and double-click it in Claude Desktop:
https://github.com/theluckystrike/mcp-servers/releases/latest
**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"time-tracker": {
"command": "npx",
"args": ["-y", "@theluckystrike/mcp-time-tracker"]
}
}
}
```
**Claude Code:**
```sh
claude mcp add time-tracker -- npx -y @theluckystrike/mcp-time-tracker
```
**Cursor** (`.cursor/mcp.json`):
```json
{
"mcpServers": {
"time-tracker": {
"command": "npx",
"args": ["-y", "@theluckystrike/mcp-time-tracker"]
}
}
}
```
The `npx` form above starts working the moment the package is published. Until then, use the .mcpb bundle above, or
build from source with exactly these three commands:
```sh
git clone https://github.com/theluckystrike/mcp-servers.git && cd mcp-servers
npm install
npm run build -w packages/mcp-license -w servers/time-tracker
```
Then point your client's `command` at `node` with one arg: the absolute path to `servers/time-tracker/dist/index.js`.
To run in Pro mode set `MCP_LICENSE_KEY` in the same config block, or call `license_activate` once with your key.
## Tools
| Tool | What it does |
| --- | --- |
| `timer_start` | Start a timer on a project (optional task, tags, rate, currency). Starting a new one stops and logs the old one. A partial project name that matches exactly one existing project is used as that project. |
| `timer_stop` | Stop the running timer, write the entry, return the duration. |
| `timer_status` | What is running, for how long, and today's total. |
| `entry_add` | Log time you already worked (start plus end or minutes), with an optional rate and currency: rate "90 euros an hour" bills as EUR 225.00 for 2.5 h. Partial project names resolve like `timer_start`. |
| `entry_list` | Compact table of entries, filtered by date range and project. |
| `entry_edit` | Change any field of an entry. |
| `entry_delete` | Delete an entry by id. |
| `project_set_rate` | Set the hourly rate and currency used for money totals (currency accepts codes or words: EUR, euros, pounds, zl). `apply_to_existing: true` re-rates time already logged for that project; add `only_missing: true` to touch only entries that carry no rate. |
| `report` | Hours and money for a period, optionally grouped by project, day, task or tag; omit `group_by` for the plain total per currency. Table, JSON or CSV. Hours already invoiced are left out; pass `unbilled_only: false` for the full timesheet. |
| `export_csv` | Write entries to a CSV file and return the path. |
| `invoice_summary` | Invoice-ready line items for one project: hours, rate, amount, total, in the currency the time was logged in. One line per task and rate, so no line ever shows a blended rate nobody agreed. Returns the `entry_ids` behind the lines, and skips hours already invoiced (`unbilled_only: false` includes them). Free for the last 7 days, Pro for any period from full history. |
| `entry_mark_billed` | Stamp the hours that went on an invoice with its number (`ids` from `invoice_summary`, or `project` + `from` + `to`), so `report` and `invoice_summary` stop offering them and the same hours are never billed twice. |
| `license_status` | Free or Pro, and where to upgrade. |
| `license_activate` | Activate a Pro key (verified offline). |
Also exposed: the resource `timetracker://today` (today's summary) and the prompt `daily_standup`
(writes a standup update from yesterday's and today's tracked time).
## What you can say
No tool names required. These are the sentences that were actually tested against the server; the tool
column is what answered them.
| You say | Tool |
| --- | --- |
| "Start a timer for the Acme website project." | `timer_start` |
| "Stop the timer and tell me how long I worked." | `timer_stop` |
| "What's running right now, and for how long?" | `timer_status` |
| "Log 2.5 hours yesterday for Acme, design review, at 90 euros an hour." | `entry_add` |
| "Show me my Acme entries for this week." | `entry_list` |
| "That last entry should say API refactor, not design review." | `entry_edit` |
| "Delete the entry I just logged twice." | `entry_delete` |
| "Set my rate for Acme to 90 EUR an hour." | `project_set_rate` |
| "How many hours did I bill this month, grouped by project?" | `report` |
| "Give me invoice lines for Acme in August." | `invoice_summary` |
| "I invoiced those hours as INV-2026-0001." | `entry_mark_billed` |
Two more worth knowing: "export my time to a CSV for my bookkeeper" (`export_csv`) and "write my standup
update from yesterday and today" (the `daily_standup` prompt).
## Worked example
This is a real transcript from the audit in `docs/USER_VALUE_R2.md`, numbers unchanged.
```
You: Log 2.5 hours yesterday for Acme, design review, at 90 euros an hour.
entry_add { project: "Acme", ... }
-> "Acme" resolved to the existing project "Acme website"
-> rate "90 euros an hour" parsed as EUR 90.00/h
Assistant: Logged 2.50 h for Acme website - design review at EUR 90.00/h = EUR 225.00.
You: How much do I bill Acme this week? Give me invoice lines.
invoice_summary { project: "Acme website", from: "2026-08-31", to: "2026-09-02" }
Assistant: Invoice summary - Acme website
design review 2.50 h EUR 90.00/h EUR 225.00
TOTAL 2.50 h EUR 225.00
```
One call each. The rate carries its currency all the way through: the report never prints a bare "225",
and it never turns into "$225" by accident.
A second worked example, the weekly report and the `daily_standup` prompt:
```
You: How many hours did I put into Acme this week, grouped by task?
report { project: "Acme website", from: "2026-08-27", to: "2026-09-02", group_by: "task" }
Assistant: Acme website, 2026-08-27 to 2026-09-02
design review 2.50 h EUR 225.00
TOTAL 2.50 h EUR 225.00
You: Write my standup update.
(prompt) daily_standup
Assistant: Yesterday: 2.50 h on Acme website (design review).
Today: nothing logged yet.
```
### Billed hours close
An hour that has been invoiced is finished. `entry_mark_billed {ids, invoice_number}` writes
`billed_at` and `billed_invoice` onto those entries; from then on `report` and `invoice_summary`
skip them by default, so next month's "invoice Acme" cannot re-bill work already paid for. The
whole timesheet is still there: pass `unbilled_only: false` to any of them. `invoice_summary`
returns the `entry_ids` it used precisely so they can be handed straight to `entry_mark_billed`
once the invoice exists.
`report` and `invoice_summary` answer overlapping questions on purpose: `report` is for "how much time
and money," grouped any way you like; `invoice_summary` is for "give me the lines I can put on an
invoice," which is a narrower, invoice-shaped view of the same entries for one project.
## How it stores data
Entries, projects and rates live in one JSON file:
`${XDG_DATA_HOME:-~/.local/share}/mcp-servers/time-tracker/data.json`.
Every write (starting or stopping a timer, adding, editing or deleting an entry, setting a rate) happens
under an advisory lock file at `.../time-tracker/.lock`, held across the whole load-mutate-save cycle, so
two overlapping calls cannot interleave and corrupt the file. The save itself writes to a temporary file
and renames it into place, so a crash or a killed process mid-write leaves either the old file or the new
one, never a half-written one. Reads (`entry_list`, `report`, `timer_status`, `export_csv`) do not take
the lock.
To back up your data, copy the single `data.json` file (and `.lock` if present, though it holds no data).
There is no database and no hidden second file.
If `data.json` is ever unreadable or not valid JSON, the server does **not** treat that as "no data yet".
It moves the file aside byte-for-byte as `data.json.corrupt-<timestamp>`, writes a `data.json.corrupt`
marker and makes every tool -- reads included -- return `data file is corrupt; moved to ...; nothing was
written`. Restore a good `data.json` (the quarantined copy is right there) and delete the marker file to
carry on. Nothing is overwritten in the meantime.
## Dates, times and rates
- **Timestamps with no offset are your local time.** `2026-09-02T09:00:00` means 09:00 where you are, not
UTC. Pass an explicit offset (`2026-09-02T09:00:00+02:00`) or a trailing `Z` and it is honoured exactly.
- **Date-only bounds cover whole local days.** `from: "2026-09-01"` is 00:00:00 local on the 1st and
`to: "2026-09-30"` is 23:59:59.999 local on the 30th, so a month reported by dates includes its last
day. Timestamps with a time are used as given.
- **Entries are clipped to the window.** An entry that starts before `from` or ends after `to` counts for
the part inside the period, not all of it and not none of it.
- **Entries are split at local midnight for day grouping.** Work from 23:30 to 01:30 is 0.5 h on the first
day and 1.5 h on the next, including across a month boundary. `timer_status` counts only the part of an
entry -- or of the running timer -- that falls after midnight today.
- **Rate strings are parsed, never guessed.** `"1,200 USD"` is 1200 (a comma followed by exactly three
digits is thousands grouping), `"12,50 EUR"` is 12.50 (the unambiguous European decimal shape), and
`"1.200,50"` is 1200.50. Anything that could mean either thing, such as `"1,2345"`, is refused with a
worked example instead of being read as the wrong number.
- **Rates are captured when the time is logged.** `entry_add` and `timer_stop` store the effective hourly
rate and currency on the entry, and reports and invoices use that stored rate. `project_set_rate`
therefore applies to future entries only; pass `apply_to_existing: true` to re-rate the time already
logged for that project. That re-stamps EVERY entry of the project, including entries that already
carry a rate, and the response says how many changed and the project's new total. Add
`only_missing: true` to touch only entries that captured no rate of their own.
- **Tag rows overlap.** In `group_by: "tag"` an entry tagged `dev` and `meeting` appears in both rows; the
total is computed from the entries once, so it is never the sum of the rows.
## Limits and honest caveats
- Free `entry_list`, `report`, `export_csv` and `invoice_summary` only see the last 7 days. Timers and
entries themselves are unlimited and nothing is ever deleted -- the window just narrows what a free
call can read back.
- Free tier supports hourly rates on 2 projects; a third rated project needs Pro.
- Every `report` grouping is free, tag included: the tag total is a correctness fix, not a premium
feature. `group_by` itself is optional -- omit it for the plain total per currency.
- Only one timer can run at a time. Starting a second one stops and logs the first -- there is no
concurrent-timer mode.
- There is no reminder or idle-detection: if you forget to stop a timer, it keeps running until you stop
it or start another.
## Troubleshooting
- **`npx` hangs or fails to find the package**: npm publish for this package is pending. Use the `.mcpb`
bundle or the clone-and-build path above until it lands.
- **Using the `.mcpb` bundle**: it installs into Claude Desktop directly; there is no separate path to
configure.
- **Using the clone path**: the server binary is `servers/time-tracker/dist/index.js` after
`npm run build`. Point your client's `command` at `node` with that absolute path as the only argument.
- **Node version**: requires Node >= 18. Check with `node -v`.
- **Nothing shows up / silent failures**: this server writes logs to stderr only, never stdout (stdout is
reserved for the MCP protocol). In Claude Desktop, check Settings -> Developer -> the server's log
file; in Claude Code, run with `--mcp-debug` or check the terminal you launched it from.
- **A Pro key isn't recognized**: run `license_status` to see what the server thinks your tier is, and
confirm `MCP_LICENSE_KEY` is set in the same process the client launches (not just your shell).
## Privacy
All data stays local: entries live in `${XDG_DATA_HOME:-~/.local/share}/mcp-servers/time-tracker/data.json`.
The server makes no network requests, has no telemetry, and needs no account. License keys are Ed25519
signatures verified offline against a public key compiled into the package -- activation works with no
internet connection.
## Pairs with
- [mcp-invoice](../invoice/README.md) -- turn `invoice_summary` output straight into a numbered PDF invoice.
- [mcp-spreadsheet](../spreadsheet/README.md) -- export a CSV with `export_csv` and query or reshape it.
- [mcp-price-tracker](../price-tracker/README.md) -- if you also buy things for the client, watch those prices.
- [office-suite](../office-suite/README.md) -- all four servers behind one install, one config entry.
- Guide: [Track billable hours in Claude Code and Cursor](https://mcp.zovo.one/guides/track-time-in-claude-code)
## FAQ
**Does this work in Cursor as well as Claude Code and Claude Desktop?**
Yes. All three speak MCP over stdio with the same config shape; the tools and the data file are identical
regardless of client.
**What happens when the free 7-day window runs out on an old entry?**
Nothing is deleted. The entry stays in `data.json` forever; it just will not appear in `entry_list`,
`report`, `export_csv` or `invoice_summary` results until you activate Pro, which opens full history.
**Can I bill different clients in different currencies?**
Yes. Currency is set per project (or per entry, overriding the project default) and every total is grouped
by currency -- a report never adds EUR and USD together.
**What happens if two entries have overlapping times?**
The server does not block overlaps; it logs what you tell it. `entry_edit` lets you fix a mistake after
the fact.
**Does it need an internet connection?**
No. There are no network calls anywhere in this server, including for license activation, which is
verified with a local public key.
## License
MIT
## One business profile for the whole suite
Your identity is stored once, at `${XDG_DATA_HOME:-~/.local/share}/mcp-servers/profile/business.json`,
and every server in the suite reads it: the invoice issuer, the docx letterhead, the recurring
issuer, expense-tracker's default VAT rate, time-tracker's and timezone's home zone, and the
resume and contract letterheads. Set it once with `business_set` (invoice or docx) - you never
repeat it anywhere else. An email address is only ever taken from that profile or from an explicit
argument; when none is stored, documents show `[add: email]` and the tool says so rather than
letting anyone improvise an address.