Skip to main content
Glama
README.md
# time-tracker-mcp

A local MCP server that lets Claude Code read your logged hours from our internal
time tracker.

Phase 0 of bench project **BP-006 — Remote MCP Skill**
([remote-mcp-skill](https://github.com/Old-St-Labs/remote-mcp-skill)). This is the
reference implementation: get the tool logic right locally, then port it to AWS.

## Why

Open Google Calendar, read last week's meetings, and transcribe
them into the tracker by hand. Claude can already see your calendar via the Google
Calendar connector — it just can't see the tracker. This closes that gap:

```
You: What meetings from last week haven't I logged yet?

Claude: [reads calendar, reads tracker, diffs]
        Logged 4h. Calendar shows 7h 45m. Missing:
        - Thu 30 Jul, Prism Standup, 15m
        - Fri 31 Jul, Masterclass, 3h
        Flag: Fri Masterclass overlaps Mentors x Interns 16:30-17:00.
```

Read-only. It tells you what to enter; it does not enter it.

## Tools

| Tool | Returns |
|---|---|
| `list_time_entries(start, end)` | entries in a date range — `day`, `start`, `hours`, `project`, `task`, `description`, `billable` |
| `list_projects()` | project ids and names |
| `get_active_timer()` | the running timer, or null |

Any range works — a day, a week, a month. Totals and grouping are left to the
caller: the tools return rows, Claude does the arithmetic.

## Install

```bash
git clone https://github.com/mikka-oldst/time-tracker-mcp.git
cd time-tracker-mcp
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
cp .env.example .env          # then fill in — see Auth below
.venv/bin/python server.py --smoke
```

`--smoke` checks the date conversions offline, then makes one live call per tool.
Run it before wiring anything up; it fails loudly and specifically.

Then register it (absolute paths — Claude spawns this as a subprocess with no
shell context):

```bash
claude mcp add -s user time-tracker -- \
  /full/path/to/time-tracker-mcp/.venv/bin/python \
  /full/path/to/time-tracker-mcp/server.py
```

Restart your session, confirm with `/mcp`, and ask it something.

## Auth

Two cookie values, pasted once. The server exchanges them for a fresh Convex JWT
on demand, so you never hand-paste a token.

DevTools → Application → Cookies → the tracker's domain:

| Cookie | `.env` key |
|---|---|
| `__session` | `CLERK_SESSION_COOKIE` |
| `__clerk_db_jwt` | `CLERK_DB_JWT` |

The Clerk session id is read from the cookie's `sid` claim, so there is nothing
else to copy. Leave `CLERK_JWT` blank — if it is set, it overrides refresh.

Tokens come from Clerk's `convex` JWT template and last an hour. The *default*
template mints 60-second tokens that expire before you can use them, which is
why the template name matters.

## How it works, and what surprised us

The tracker is Next.js on Vercel with Clerk auth and a **Convex** backend. It has
no REST API — the browser talks to Convex over a WebSocket. Probing `/api/*`
returns 404 for everything, and the Network tab looks empty unless you filter to
`WS` and read the frames.

Function names and argument shapes came from the `ModifyQuerySet` frames in that
WebSocket. `timeEntries:listByWeek` takes an arbitrary range despite its name —
the UI calls it with a single day.

Three things worth knowing if you extend this:

- **Epoch milliseconds, Manila midnight.** Not UTC. Getting it wrong shifts
  entries by a day. Asserted against real captured frames in `smoke()`.
- **Convex returns HTTP 200 on function failure.** Check `status` in the response
  body, not the status code.
- **Entries carry 21 fields**, most of it Clockify/ClickUp sync plumbing. They get
  trimmed to 7 before reaching Claude; passing the rest floods the context.

We use `httpx` against Convex's HTTP query endpoint rather than the official
`convex` Python client, which hung indefinitely with no timeout option.

## Limitations

- **Local only.** The cookie-refresh flow uses Clerk's undocumented client API and
  authenticates as *you*. It cannot run on Lambda. A remote deployment needs a
  Convex deploy key or a Clockify API key.
- **Read-only.** No writing entries. A human should confirm hours.
- **Calendar shows invitations, not attendance.** RSVPs are often `needsAction`, so
  a meeting on the calendar is not proof you attended it.
- **Overlapping meetings need a human rule.** The tools surface the conflict rather
  than guessing which one wins.