Productive.io Remote MCP Server
A remote MCP server for Productive.io running on Cloudflare Workers. Connects Claude Desktop, Claude Code, and other MCP-compatible clients to your Productive.io workspace — no local installation, no API keys on client machines.
Highlights
- Remote-first — runs on Cloudflare Workers, clients connect via URL
- Microsoft Entra ID — OAuth 2.1 with your organization's identity provider (SSO + MFA)
- Per-user authorization (BYOT) — every call uses the user's own Productive token, encrypted at rest; no shared admin token (details)
- Auto user resolution — maps Entra email to Productive user ID automatically
- Name resolution — people, companies, projects, tasks, and deals are shown by name, not raw ID
- 70+ tools — projects, tasks, time tracking, invoicing, comments, pages, and more
- Zero client setup — just add one URL to Claude Desktop or Claude Code
Quick Start
If someone in your organization has already deployed this server, you only need to configure your MCP client:
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"productive": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://<your-worker>.<your-subdomain>.workers.dev/mcp"]
}
}
}
Restart Claude Desktop. A browser window opens for Entra ID login on first connect. The session persists — you only sign in once.
Claude Code
claude mcp add productive -- npx -y mcp-remote https://<your-worker>.<your-subdomain>.workers.dev/mcp
Deploy Your Own
Prerequisites
- Cloudflare account with Workers (free tier works)
- Microsoft Entra ID tenant with App Registration rights
- Node.js 18+ and npm
1. Clone and install
git clone https://github.com/MonadsAG/monads-mcp-productive.git
cd monads-mcp-productive
npm install
2. Create Cloudflare KV namespaces
npx wrangler login
npx wrangler kv namespace create "OAUTH_KV"
npx wrangler kv namespace create "USER_MAPPING_KV"
npx wrangler kv namespace create "USER_PAT_KV"
Copy the namespace IDs into wrangler.jsonc. (USER_PAT_KV stores each user's
encrypted Productive token — see Per-user tokens.)
3. Register an Entra ID application
# Login to your Entra tenant
az login --tenant <your-tenant-id> --allow-no-subscriptions
# Create the app registration (single tenant).
# Register BOTH redirect URIs: /callback (MCP login) and /settings/callback (token settings page).
az ad app create \
--display-name "Productive MCP" \
--sign-in-audience "AzureADMyOrg" \
--web-redirect-uris \
"https://<your-worker>.<your-subdomain>.workers.dev/callback" \
"https://<your-worker>.<your-subdomain>.workers.dev/settings/callback"
# Note the appId from the output, then add API permissions
az ad app permission add \
--id <app-id> \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions \
37f7f235-527c-4136-accd-4a02d197296e=Scope \
14dad69e-099b-42c9-810b-d002981feec1=Scope \
64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0=Scope \
e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope
# Grant admin consent
az ad app permission admin-consent --id <app-id>
# Generate a client secret (2-year expiry)
az ad app credential reset \
--id <app-id> \
--append \
--display-name "Cloudflare Worker" \
--years 2
The permission IDs correspond to: openid, profile, email, User.Read (delegated).
4. Set Cloudflare secrets
npx wrangler secret put PRODUCTIVE_ORG_ID # your org ID with slug (e.g. 12345-company-name)
npx wrangler secret put PRODUCTIVE_API_BASE_URL # https://api.productive.io/api/v2/ (or sandbox URL)
npx wrangler secret put PRODUCTIVE_TOOLSETS # optional, comma-separated toolset names (default: all tools)
npx wrangler secret put ENTRA_CLIENT_ID # app (client) ID from step 3
npx wrangler secret put ENTRA_CLIENT_SECRET # client secret from step 3
npx wrangler secret put ENTRA_TENANT_ID # your Entra directory (tenant) ID
openssl rand -hex 32 | npx wrangler secret put COOKIE_ENCRYPTION_KEY # signs session/CSRF cookies
openssl rand -hex 32 | npx wrangler secret put PAT_ENC_KEY # encrypts per-user PATs at rest (AES-256-GCM)
No shared Productive token. Each user supplies their own Productive PAT via the
/settingspage (see Per-user tokens), soPRODUCTIVE_API_TOKENis not needed by the Worker. It remains only for the legacy local stdio entrypoint (set it in.env, not as a Worker secret). There,PRODUCTIVE_API_TOKENandPRODUCTIVE_ORG_IDare enough: the person ID behind "me" is resolved from the token at startup, soPRODUCTIVE_USER_IDis only needed to override it.
5. Deploy
npm run worker:deploy
6. Connect
Configure Claude Desktop or Claude Code as shown in Quick Start, replacing the placeholder URL with your Worker URL.
Per-user tokens (BYOT)
Every Productive API call runs with the requesting user's own Personal Access Token (Bring Your Own Token), so Productive enforces each user's real permissions — there is no shared admin token. Identity still comes from Microsoft Entra ID; only the upstream credential is now per-user.
After connecting the MCP, each user opens the settings page once to store their token:
https://<your-worker>.<your-subdomain>.workers.dev/settings
The page signs the user in with Entra ID, then lets them set, rotate, or delete their
Productive PAT — no MCP reconnect required. The token is validated against Productive,
encrypted with AES-256-GCM (PAT_ENC_KEY) before being written to USER_PAT_KV (keyed by
the user's stable Entra object ID), and never displayed, logged, or sent through the model.
Until a user stores a token, their tool calls return a friendly message linking to
/settings instead of any data.
The settings page and the sign-in dialog render in German or English automatically, based on
the browser's Accept-Language (default English); append ?lang=de / ?lang=en to force one.
Architecture
┌─────────────────┐ Streamable HTTP ┌──────────────────────────────┐
│ Claude Desktop │ ◄──────────────────► │ Cloudflare Worker │
│ / Claude Code │ │ │
│ (via mcp-remote)│ │ OAuthProvider (OAuth 2.1) │
└─────────────────┘ │ ↓ │
│ Entra ID (OIDC login) │
Browser ←── login redirect ───────→│ ↓ │
│ McpAgent (Durable Object) │
│ ↓ │
│ ProductiveAPIClient │
└──────────────┬───────────────┘
↓
Productive.io API
Available Tools
Company & Project
| Tool | Description |
|---|---|
whoami | Get current user context |
list_companies | List companies/customers |
list_projects | List projects with status filtering |
Folders (Boards) & Task Lists
Productive's UI calls this resource a "folder"; the API models it as a "board" (tasks and task lists reference their parent via a board/board_id relationship). One tool set covers it -- there is no separate "board" tool.
| Tool | Description |
|---|---|
list_folders / get_folder / create_folder / update_folder | Folder CRUD |
archive_folder / restore_folder / copy_folder / move_folder / reposition_folder | Folder lifecycle & positioning |
list_task_lists / create_task_list / get_task_list / update_task_list | Task list CRUD |
archive_task_list / restore_task_list / copy_task_list / move_task_list / reposition_task_list | Task list operations |
Tasks
| Tool | Description |
|---|---|
list_tasks / get_task / get_project_tasks / my_tasks | Query tasks |
create_task / update_task / delete_task | Task CRUD |
update_task_status | Task state changes |
list_custom_fields / list_custom_field_options | Custom field discovery |
move_task_to_list / add_to_backlog / reposition_task | Task positioning |
list_subtasks / create_subtask | Subtask management |
list_task_dependencies / create_task_dependency / delete_task_dependency | Dependencies |
Comments & Todos
| Tool | Description |
|---|---|
add_task_comment / list_comments / get_comment / update_comment / delete_comment | Comment CRUD |
pin_comment / unpin_comment / add_comment_reaction | Comment actions |
list_todos / get_todo / create_todo / update_todo / delete_todo | Todo management |
Pages & Documents
| Tool | Description |
|---|---|
list_pages / get_page / create_page / update_page / delete_page | Page CRUD |
list_page_children | Page hierarchy |
move_page / copy_page | Page operations |
Time Tracking
| Tool | Description |
|---|---|
list_time_entries / create_time_entry / update_time_entry | Time entry CRUD |
set_time_entry_approval | Approval workflow (action param) |
start_timer / stop_timer / get_timer / get_running_timer | Real-time timers |
list_services / get_project_services / list_project_deals / list_deal_services | Budget & service lookup |
create_budget_service / update_budget_service | Budget service (line item) CRUD |
list_budget_sections / create_budget_section / update_budget_section / delete_budget_section | Budget sections (service groups) |
Invoicing
| Tool | Description |
|---|---|
list_invoices / get_invoice / create_invoice / update_invoice / delete_invoice | Invoice CRUD |
list_company_budgets / create_budget / update_budget / create_budget_from_deal | Budget CRUD & derivation |
generate_line_items / finalize_invoice / mark_invoice_paid | Invoice workflow |
get_invoice_pdf_url / get_timesheet_report_url | Document URLs |
Activity & Workflow
| Tool | Description |
|---|---|
list_activities | List activities with filters |
get_recent_updates | Get recent updates |
list_workflow_statuses | List workflow statuses |
Common Workflows
Time Entry
Follow the hierarchy: Project → Deal/Budget → Service → Time Entry.
list_projects → list_project_deals → list_deal_services → create_time_entry
Task Status Update
Update by name — no ID lookup needed:
{ "task_id": "123", "status_name": "In Progress" }
Invoice
list_companies → list_company_budgets → create_invoice → generate_line_items → finalize_invoice → mark_invoice_paid
Budget
Direct creation:
list_companies → create_budget → update_budget (optional)
Derived from an existing deal/contract (the deal must be in a "Won" status):
list_project_deals → create_budget_from_deal
Budget Services
Attach services (line items) to a budget:
create_budget → create_budget_service → update_budget_service (optional)
{ "budget_id": "123", "name": "Consulting Hours", "price": 150, "quantity": 20 }
unit_id (1=Hour, 2=Piece, 3=Day) defaults to 1; billing_type_id (1=Fixed, 2=Actuals, 3=None, 4=Percentage) defaults to 2.
To group a budget's services, for example by project phase, create sections with
create_budget_section and pass section_id to create_budget_service or update_budget_service.
list_budget_sections shows each section with its services; delete_budget_section deletes only
an empty section.
npm package / MCP hub
The stdio entry point is also published to npm as
@monadsag/productive-mcp, for hosts
that start MCP servers as local processes (such as the Monads MCP hub). Every push to main
publishes a new version from CI with npm provenance: major and minor come from package.json,
the patch is the CI run number. There is no manual release step. Each release ships an
npm-shrinkwrap.json, so every install gets exactly the dependency tree CI tested.
npx -y @monadsag/productive-mcp@latest
| Variable | Required | Description |
|---|---|---|
PRODUCTIVE_API_TOKEN | yes | The user's own Productive API token |
PRODUCTIVE_ORG_ID | yes | Organization ID with slug, e.g. 12345-company-name |
PRODUCTIVE_USER_ID | no | Person ID behind "me"; resolved from the token if unset |
PRODUCTIVE_TOOLSETS | no | Comma-separated toolsets to enable (default: all) |
The server speaks MCP on stdout and logs to stderr only. Note that the unscoped
productive-mcp on npm is the upstream original by berwickgeek, not this fork.
npm run build && npm run package:smoke packs the package, serves it from a throwaway local
registry and starts it with npx over stdio, the same way the hub does.
Development
npm run worker:dev # local dev on port 8788 (requires .dev.vars with secrets)
npm run worker:deploy # deploy to Cloudflare
npm run build # compile TypeScript (for local stdio fallback)
npm run format # prettier
Create a .dev.vars file (gitignored) with the same variables as the Cloudflare secrets for local development — including PAT_ENC_KEY (a 64-char hex string, e.g. openssl rand -hex 32). wrangler dev provisions local KV namespaces automatically.
License
Originally based on productive-mcp by jayat3dn (ISC). See NOTICE for attribution details.
Maintained by Monads AG