productive-mcp
# productive-mcp
An [MCP](https://modelcontextprotocol.io/) server for [Productive.io](https://productive.io) — log time, inspect projects, manage entries, and get team reports from any MCP-compatible client (Claude Code, Claude Desktop, Cursor, etc.) using plain English:
> "log 2.5 hours on the Acme security review project"
>
> "how many hours did Alice log this week?"
>
> "give me my weekly briefing"
Built around the Productive.io JSON:API v2 with fuzzy project and person matching, a local disk cache, billing-cutoff-aware period resolution, and per-project default-service memory.
---
## Features
- **14 tools** covering projects, people, services, time entries, reports, and briefings
- **Cross-platform credential storage** — macOS Keychain, Windows Credential Manager (DPAPI/TPM-backed), Linux Secret Service, or environment variables
- **Fuzzy matching** for projects and people — `"Acme"`, `"1099 Acme"`, `"Alice"`, or `"me"` all resolve naturally
- **Billing-cutoff-aware period resolution** — `"this_month"` shifts based on your invoicing cycle
- **Remembers your default service per project** so you don't have to specify it every time
- **Local cache** for projects, people, and services (1-hour TTL, manually refreshable)
- **TOON output mode** — optional token-optimised encoding for 30–60% fewer tokens
- **`hours` in, `hours` out** — the API uses minutes internally, but you never see them
- **Scoped to "me" by default** — `list_time_entries` only shows your own entries unless you opt out
## Requirements
- Python **3.11+**
- A Productive.io account with API access enabled
- macOS, Windows, or Linux
## Install
### 1. Clone and install
```bash
git clone https://github.com/cameronfairbairn/productive-mcp.git
cd productive-mcp
uv venv
uv pip install -e .
```
(or `python -m venv .venv && .venv/bin/pip install -e .` if you don't use [uv](https://github.com/astral-sh/uv))
### 2. Get your Productive credentials
You need three values:
| Value | Where to find it |
|---|---|
| **API token** | Productive → Settings → API integrations → **Generate new token** |
| **Organization ID** | The numeric segment in your Productive URL: `app.productive.io/<ORG_ID>/…` |
| **Person ID** | Your own user ID — open your profile in Productive; it's the numeric segment in the URL |
### 3. Store credentials
#### Option A — OS credential store (recommended)
Works on any platform via the `keyring` library:
```bash
python -c "import keyring; keyring.set_password('productive-mcp', 'token', '<token>')"
python -c "import keyring; keyring.set_password('productive-mcp', 'org_id', '<org_id>')"
python -c "import keyring; keyring.set_password('productive-mcp', 'person_id', '<person_id>')"
```
| Platform | Backend | Security |
|---|---|---|
| macOS | Keychain | Secure Enclave where available |
| Windows | Credential Manager (DPAPI) | TPM-backed on TPM 2.0 systems |
| Linux (desktop) | Secret Service (GNOME Keyring / KWallet) | Session-encrypted |
On macOS, the `security` CLI also works (keyring reads from the same Keychain):
```bash
security add-generic-password -s productive-mcp -a token -w "<token>" -U
security add-generic-password -s productive-mcp -a org_id -w "<org_id>" -U
security add-generic-password -s productive-mcp -a person_id -w "<person_id>" -U
```
#### Option B — environment variables (CI / headless / override)
```bash
export PRODUCTIVE_MCP_TOKEN="<token>"
export PRODUCTIVE_MCP_ORG_ID="<org_id>"
export PRODUCTIVE_MCP_PERSON_ID="<person_id>"
```
Environment variables take precedence over the credential store.
### 4. Register the server with your MCP client
#### Claude Code (`~/.claude.json`)
```json
{
"mcpServers": {
"productive": {
"type": "stdio",
"command": "/path/to/productive-mcp/.venv/bin/productive-mcp",
"args": []
}
}
}
```
#### Claude Desktop (`claude_desktop_config.json`)
```json
{
"mcpServers": {
"productive": {
"command": "/path/to/productive-mcp/.venv/bin/productive-mcp"
}
}
}
```
Restart the client after editing its config.
### 5. (Optional) Global install with the bundled deploy script
```bash
bash scripts/install.sh
```
This creates `~/.local/share/productive-mcp/` containing a fresh venv and a `run.sh` launcher. Point your MCP client at `~/.local/share/productive-mcp/run.sh` instead. Re-running the script upgrades in place.
---
## Tools
All tools are prefixed with `productive_` so they namespace cleanly alongside other MCP servers.
### Time tracking
| Tool | Purpose |
|---|---|
| `productive_log_time` | Create a time entry |
| `productive_list_time_entries` | List time entries with date/project/owner filters |
| `productive_update_time_entry` | Edit hours / date / note / service on an existing entry |
| `productive_delete_time_entry` | Permanently delete a time entry |
### Reports and briefings
| Tool | Purpose |
|---|---|
| `productive_get_time_report` | Categorised hours summary (worked/client/internal/holidays) |
| `productive_get_employee_hours` | Hours summary for any team member by name |
| `productive_my_projects` | Projects you've logged time on recently |
| `productive_my_briefing` | Weekly summary with hours-by-project breakdown |
### Projects, people, and services
| Tool | Purpose |
|---|---|
| `productive_list_projects` | List all active projects |
| `productive_find_project` | Fuzzy-search projects by name and/or number |
| `productive_find_person` | Fuzzy-search people by name or email |
| `productive_list_services` | List services (billable activity types) on a project |
### Administration
| Tool | Purpose |
|---|---|
| `productive_refresh_cache` | Force an immediate cache refresh |
| `productive_set_default_service` | Override the remembered default service for a project |
### Key tool details
#### `productive_get_time_report`
```
period : str? — "this_month", "last_month", "this_week", "last_week"
after : str? — ISO date; ignored if period set
before : str? — ISO date; ignored if period set
person : str? — Name, email, id, or "me" (default)
project : str? — Scope to a specific project
```
Returns `totals_hours: {worked, client, internal, holidays}`. When `PRODUCTIVE_BILLING_CUTOFF_DAY` is set, "this_month" shifts based on your billing cycle.
#### `productive_my_briefing`
```
period : str — Default "this_week"
```
Returns total hours, projects touched, hours-by-project breakdown, and recent entries. Designed for the Monday-morning "what am I working on?" question.
#### `productive_get_employee_hours`
```
person : str — Fuzzy name, email, or "me"
period : str? — Symbolic period (default: no filter)
after : str? — ISO date
before : str? — ISO date
```
Finds a person by fuzzy match, returns their hours for the period.
---
## Configuration
### Environment variables
| Variable | Default | Purpose |
|---|---|---|
| `PRODUCTIVE_MCP_TOKEN` | — | API token (overrides credential store) |
| `PRODUCTIVE_MCP_ORG_ID` | — | Organization ID (overrides credential store) |
| `PRODUCTIVE_MCP_PERSON_ID` | — | Person ID (overrides credential store) |
| `PRODUCTIVE_BILLING_CUTOFF_DAY` | unset | Day of month (1–31) when "this_month" flips to the current calendar month |
| `PRODUCTIVE_MCP_OUTPUT_FORMAT` | `json` | Set to `toon` for Token-Optimized Object Notation (30–60% fewer tokens) |
### Billing cutoff day
The `PRODUCTIVE_BILLING_CUTOFF_DAY` controls how symbolic periods resolve for invoicing workflows:
- **Before cutoff day**: `"this_month"` = previous calendar month (you're still closing invoices)
- **From cutoff day onward**: `"this_month"` = current calendar month
- **Unset**: `"this_month"` always means the current calendar month
Example: with `PRODUCTIVE_BILLING_CUTOFF_DAY=10`, on April 5th, `"this_month"` resolves to March 1–31.
---
## Security & permissions
### Credential storage
API tokens are **never written to disk** by this project. They live in the OS credential store (Keychain / Credential Manager / Secret Service) or in environment variables.
### Productive's permission model
Each team member generates their own API token via Productive → Settings → API integrations ([docs](https://help.productive.io/en/articles/5440689-api-access-with-personal-access-tokens)). Tokens inherit the generating user's permissions:
> *"API tokens inherit the same access restrictions as the user they are associated with. If a user does not have permission to access certain features or data within Productive, these limitations will also apply to their API token."*
Tokens also have a **read-only vs read/write** scope chosen at creation time.
**What this means for team deployments:**
- A "Member"-role user's token **cannot** query other members' time entries — Productive returns 403.
- The real access boundary is the **Productive role** (Settings → People → Role), not MCP-side configuration.
- Tools like `productive_get_employee_hours` will simply return empty/error for users whose Productive role doesn't grant team visibility.
For more on roles: [User Permissions Overview](https://help.productive.io/en/articles/3524646-permissions-by-role) | [Productive API docs](https://developer.productive.io/)
---
## How it works
### Architecture
```
┌─────────────────────┐ stdio ┌──────────────────────┐ HTTPS ┌─────────────────┐
│ MCP client │ ─────────► │ productive-mcp │ ────────► │ Productive.io │
│ (Claude Code etc.) │ │ (FastMCP server) │ │ JSON:API v2 │
└─────────────────────┘ └──────────┬───────────┘ └─────────────────┘
│
▼
~/.config/productive-mcp/
├── cache.json (projects, people, services)
└── preferences.json (per-project default service)
```
### Credential lookup order
1. Environment variable (`PRODUCTIVE_MCP_TOKEN` / `_ORG_ID` / `_PERSON_ID`)
2. OS credential store via `keyring` (Keychain / Credential Manager / Secret Service)
3. Error with a helpful message pointing at both options
### Local state
Two files under `~/.config/productive-mcp/`, both `0600`:
- `cache.json` — projects, people (active only), per-project services. 1-hour TTL.
- `preferences.json` — `{ "default_services": { "<project_id>": "<service_id>" } }`.
Neither file ever contains credentials.
---
## Development
```bash
uv pip install -e ".[dev]"
# unit tests (no network required)
pytest
# integration tests (hit the real Productive API — requires credentials)
pytest -m integration
# lint + type check
ruff check .
mypy src
```
### Project layout
```
src/productive_mcp/
├── __main__.py Entrypoint (python -m productive_mcp)
├── server.py FastMCP tool definitions (14 tools)
├── client.py Async Productive.io API client + fuzzy matcher
├── auth.py Cross-platform credential loader (keyring + env)
├── storage.py Local cache + preferences persistence
├── periods.py Billing-cutoff-aware period resolution
└── reporting.py Time entry categorisation for reports
scripts/
└── install.sh One-shot deploy script for the global launcher
tests/
├── test_client.py Fuzzy matching + trim functions
├── test_periods.py Period resolution + billing cutoff
├── test_reporting.py Entry categorisation
└── test_formatter.py TOON output encoding
```
---
## Troubleshooting
**"Credential store lookup failed"**
The `keyring` backend isn't configured or accessible. Check: `python -c "import keyring; print(keyring.get_keyring())"`. On headless Linux, use environment variables instead.
**"No credential found for service=productive-mcp"**
Credentials haven't been stored yet. Run the `keyring.set_password(...)` commands from the install section.
**"No project matches 'X'"**
Cache may be stale. Call `productive_refresh_cache` and retry.
**"Ambiguous project query"**
Two projects scored nearly identically. Be more specific — add the project number.
**"Project has multiple services; pass service_hint"**
Either pass `service_hint="…"` (it'll be remembered), or call `productive_set_default_service` once upfront.
---
## Alternatives
Several other Productive.io MCP servers exist:
- [berwickgeek/productive-mcp](https://github.com/berwickgeek/productive-mcp) — the most feature-complete general-purpose server (projects, tasks, boards, people, workflows). Node.js. No time tracking.
- [adamchrabaszcz/productive-time-mcp](https://github.com/adamchrabaszcz/productive-time-mcp) — companion time-tracking server for the above.
- [druellan/Productive-Simple-MCP](https://github.com/druellan/Productive-Simple-MCP) — read-only Python/FastMCP server using TOON output for low token usage.
- [laurkee/productive-mcp](https://codeberg.org/laurkee/productive-mcp) (Codeberg) — ticketing-focused.
**Why pick this one?**
1. **Cross-platform credential store** — not a `.env` file. macOS Keychain, Windows Credential Manager (TPM-backed), Linux Secret Service.
2. **Fuzzy matching for projects and people** — say `"Alice"` or `"1099 Acme"` instead of looking up IDs.
3. **Per-project default service memory** — log time without specifying the service after the first call.
4. **Billing-cutoff-aware reports** — `"this_month"` means what your invoicing cycle says it means.
5. **TOON output mode** — 30–60% fewer tokens for long sessions.
## License
MIT — see [LICENSE](LICENSE).
## Contributing
Issues and PRs welcome. Please include tests for new behaviour.
---
Not affiliated with Productive.io. "Productive" is a trademark of its respective owners.
TDQS
Scored across 14 tools
Each tool has a clearly distinct purpose: searching people, searching projects, listing/creating/updating/deleting time entries, reporting hours, managing cache, etc. No two tools appear to do the same thing.
All tools follow a consistent verb_noun pattern with snake_case (e.g., productive_list_time_entries, productive_log_time). Even 'my_briefing' and 'my_projects' fit the pattern as personal retrieval operations.
14 tools is an appropriate number for a time-tracking MCP server, covering core operations without being overwhelming. Each tool serves a necessary function.
The tool set covers CRUD for time entries, project and person search, reporting, and cache management. Minor gaps exist (e.g., no direct 'get project details' tool, but list_projects provides adequate info).