Skip to main content
Glama

productive-mcp

An MCP server for Productive.io — log time, inspect projects, and manage time entries from any MCP-compatible client (Claude Code, Claude Desktop, Cursor, etc.) using plain English:

"log 2.5 hours on the Acme security review project"

"show me my time entries for last week"

"delete time entry 123456"

Built around the Productive.io JSON:API v2 with fuzzy project matching, a local disk cache, and per-project default-service memory so the common case — logging time to a project you use regularly — is a single sentence.


Features

  • 9 tools covering projects, services, and time entries (list / create / update / delete)

  • Fuzzy project matching"Acme", "1099 Acme", or "1099" all resolve to the same project

  • Remembers your default service per project so you don't have to specify it every time

  • Local cache for projects and services (1-hour TTL, manually refreshable)

  • Credential safety: API token is read from the macOS Keychain by default (environment variables supported as a fallback for Linux/WSL or headless use)

  • hours in, hours out — the API uses minutes internally, but you never see them

  • Scoped to "me" by defaultlist_time_entries only shows your own entries unless you opt out

Requirements

  • Python 3.11+

  • A Productive.io account with API access enabled

  • macOS (recommended, for Keychain integration) — Linux / WSL work via environment variables

Install

1. Clone and install

git clone https://github.com/<you>/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)

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

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

The server looks these up at startup via the security CLI. They are never written to disk by this project.

Option B — environment variables (Linux / WSL / CI / override)

export PRODUCTIVE_MCP_TOKEN="<token>"
export PRODUCTIVE_MCP_ORG_ID="<org_id>"
export PRODUCTIVE_MCP_PERSON_ID="<person_id>"

Environment variables take precedence over Keychain lookups, so they're also the simplest way to test alternate accounts temporarily.

4. Register the server with your MCP client

Claude Code (~/.claude.json)

Add under mcpServers:

{
  "mcpServers": {
    "productive": {
      "type": "stdio",
      "command": "/path/to/productive-mcp/.venv/bin/productive-mcp",
      "args": []
    }
  }
}

Claude Desktop (claude_desktop_config.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

If you want one shared install that isn't tied to a clone directory:

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 of the venv binary. Re-running the script upgrades the venv in place.


Tools

All tools are prefixed with productive_ so they namespace cleanly alongside other MCP servers.

Tool

Purpose

productive_list_projects

List all active projects (id, name, number, company)

productive_find_project

Fuzzy-search projects by name and/or number

productive_list_services

List services (billable activity types) on a given project

productive_log_time

Create a time entry

productive_list_time_entries

List time entries with optional 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

productive_refresh_cache

Force an immediate cache refresh (after creating new projects/services)

productive_set_default_service

Override the remembered default service for a project

Tool reference

productive_log_time

project       : str   — Project name (fuzzy) or numeric id. e.g. "1099 Acme", "42"
hours         : float — Hours worked (e.g. 2.5). Converted to minutes internally.
note          : str?  — Description of the work
date          : str?  — ISO date (YYYY-MM-DD). Defaults to today.
service_hint  : str?  — Service name or id (only needed when the project has
                        multiple services and no remembered default)

On success, returns the created entry plus a service_resolution note explaining how the service was chosen ("auto-selected only service" / "used remembered default" / "matched by name" etc.).

If the project has only one service, it's auto-selected and saved as the default for next time. If it has multiple, the server returns an actionable error listing the options.

productive_find_project

query : str — Partial name or number (e.g. "Acme", "1099 acme", "1099")
limit : int — Max matches (default 5)

Returns scored matches sorted best-first. The fuzzy scorer combines token substring matching, a sequence-matcher fallback for typos, and a boost when the query contains an exact project number.

productive_list_time_entries

after     : str?  — Include entries on/after this ISO date
before    : str?  — Include entries on/before this ISO date
project   : str?  — Filter by project name or id
mine_only : bool  — Default True; set False to see the whole team's entries

Entries are returned newest-first, with totals rolled up in total_hours.


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, services; 1h TTL)
                                    └── preferences.json  (per-project default service)

Key design decisions

  • Services are scoped to Deals, which belong to Projects. Productive's data model makes this visible; the MCP walks the hierarchy transparently so "services on project X" Just Works.

  • Hours are the only user-facing unit. The API stores time in minutes; this layer converts at the boundary.

  • Remembered defaults reduce back-and-forth. After you log time to a project once, subsequent calls can omit service_hint — the default is remembered per project in preferences.json.

  • Fuzzy matching over exact matching. Most time-logging requests say something like "that security review for Acme", not "project #1099". The resolver biases toward natural language.

  • Mine-only by default. You're almost always logging time for yourself; the one person who needs team-wide visibility can pass mine_only=False.

Local state

Two files live under ~/.config/productive-mcp/, both created with 0600 permissions:

  • cache.json — trimmed project list and per-project services lookup. Refreshes automatically on TTL expiry or via productive_refresh_cache.

  • preferences.json{ "default_services": { "<project_id>": "<service_id>" } }.

Neither file ever contains credentials.

Credential lookup order

  1. Environment variable (PRODUCTIVE_MCP_TOKEN / _ORG_ID / _PERSON_ID)

  2. macOS Keychain (security find-generic-password -s productive-mcp -a <account>)

  3. Error — the server refuses to start with a helpful message pointing at both options.


Development

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

Run the server directly for debugging:

.venv/bin/productive-mcp
# or
python -m productive_mcp

It talks stdio JSON-RPC, so to exercise it by hand you'll want an MCP client or mcp-inspector.

Project layout

src/productive_mcp/
├── __main__.py      Entrypoint (`python -m productive_mcp`)
├── server.py        FastMCP tool definitions
├── client.py        Async Productive.io API client + fuzzy matcher
├── auth.py          Keychain / env-var credential loader
└── storage.py       Local cache + preferences persistence
scripts/
└── install.sh       One-shot deploy script for the global launcher
tests/
└── test_client.py   Unit tests for trimming + fuzzy matching

Troubleshooting

"Keychain lookup failed" Either the item doesn't exist yet (re-run the security add-generic-password commands in step 3) or you're not on macOS. Use environment variables instead.

"No project matches 'X'" Your cache may be stale if the project was created recently. Call productive_refresh_cache and try again.

"Ambiguous project query" Two projects scored nearly identically. Be more specific — add the project number, or the company name.

"Project has multiple services; pass service_hint" The project hasn't had a default set yet. Either pass service_hint="…" on this call (it'll be remembered), or call productive_set_default_service once upfront.

The server starts, but the client reports "no tools" Make sure your MCP client is pointing at the venv's productive-mcp binary (or the run.sh launcher), not at a source file. FastMCP advertises tools at handshake time — if the process can't import mcp, no tools will appear.


Security notes

  • API tokens are never written to disk by this project. They live in the Keychain or in environment variables only.

  • The cache and preferences files are created with 0600 permissions and contain no credentials.

  • The Productive API token grants the same access your user has. Treat it accordingly. Rotate via Productive → Settings → API integrations if you suspect exposure.

Alternatives

Several other Productive.io MCP servers exist — worth checking before adopting this one:

Why pick this one? This server is narrowly focused on the "log time and manage entries" workflow, and the UX is built around three opinionated choices:

  1. Credentials in the macOS Keychain by default, not a .env — no risk of a token ending up in git status.

  2. Fuzzy project matching with project-number boost — say "1099 Acme" or just "Acme"; both work.

  3. Per-project default service memory — after the first productive_log_time on a project, subsequent calls don't need to specify a service.

If you primarily need tasks/boards/workflows rather than time logging, use berwickgeek/productive-mcp instead — or run both side-by-side (they use different tool prefixes).

License

MIT — see LICENSE.

Contributing

Issues and PRs welcome. This is a small project; please keep changes focused and include tests for any new behaviour in client.py.


Not affiliated with Productive.io. "Productive" is a trademark of its respective owners.

Install Server
A
security – no known vulnerabilities
A
license - permissive license
A
quality - A tier

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cameronfairbairn/productive-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server