Skip to main content
Glama
README.md
# 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

A4/5.0

Scored across 14 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

14 tools is an appropriate number for a time-tracking MCP server, covering core operations without being overwhelming. Each tool serves a necessary function.

Completeness4/5

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).

Maintenance

ActivityInactive
ResponsivenessNo issues