Skip to main content
Glama
README.md
# toggl-focus-mcp

MCP server for **[Toggl Focus (2.0)](https://engineering.toggl.com/docs/focus/)** — not Toggl Track 1.0.

Connect Claude, Cursor, Codex, or OpenCode to your Focus workspace with a Bearer API key (`toggl_sk_…`) and `npx`. No OAuth browser flow required.

```bash
npx -y toggl-focus-mcp
```

| | |
| --- | --- |
| **Auth** | Focus Bearer API key (`Authorization: Bearer toggl_sk_…`) |
| **API** | `https://focus.toggl.com/api` |
| **Transport** | stdio MCP |
| **Node** | ≥ 20 (CI / `.nvmrc` → latest Current) |

> Track “API Token” values (Basic Auth / profile token for Track) **do not work**. Create a Focus / Toggl 2.0 key in settings. Official OAuth MCP: [`@togglhq/mcp`](https://www.npmjs.com/package/@togglhq/mcp).

---

## Prerequisites

1. A **Toggl Focus / 2.0 API key** (`toggl_sk_…`) — shown once when created  
2. Your **organization ID** and **workspace ID** (strongly recommended)  
3. **Node.js 20+** (for `npx`)

Find org/workspace IDs in the Focus URL, e.g.  
`https://focus.toggl.com/.../organizations/21504582/workspaces/21503801/...`

Setting both IDs avoids extra discovery calls and protects against Focus hourly quota limits.

---

## Environment variables

| Variable | Required | Description |
| --- | --- | --- |
| `TOGGL_FOCUS_API_TOKEN` | Yes\* | Focus API key (`toggl_sk_…`) |
| `TOGGL_API_KEY` | Yes\* | Alias for the Focus API key |
| `TOGGL_API_TOKEN` | Yes\* | Additional alias |
| `TOGGL_ORGANIZATION_ID` | Recommended | Default organization ID |
| `TOGGL_WORKSPACE_ID` | Recommended | Default workspace ID |
| `TOGGL_FOCUS_BASE_URL` | No | API base (default `https://focus.toggl.com/api`) |

\* Exactly one of `TOGGL_FOCUS_API_TOKEN`, `TOGGL_API_KEY`, or `TOGGL_API_TOKEN` is required.

Most tools accept optional `organization_id` / `workspace_id` arguments that override the env defaults for that call.

---

## Install

Replace `toggl_sk_...`, `123`, and `456` with your real key and IDs.

### Claude Code

```bash
claude mcp add toggl-focus \
  -e TOGGL_FOCUS_API_TOKEN=toggl_sk_... \
  -e TOGGL_ORGANIZATION_ID=123 \
  -e TOGGL_WORKSPACE_ID=456 \
  -- npx -y toggl-focus-mcp
```

Verify:

```bash
claude mcp list
```

### Claude Desktop

Edit `claude_desktop_config.json`, then restart Claude Desktop.

- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "toggl-focus": {
      "command": "npx",
      "args": ["-y", "toggl-focus-mcp"],
      "env": {
        "TOGGL_FOCUS_API_TOKEN": "toggl_sk_...",
        "TOGGL_ORGANIZATION_ID": "123",
        "TOGGL_WORKSPACE_ID": "456"
      }
    }
  }
}
```

### Cursor

Add to project `.cursor/mcp.json` or your global Cursor MCP settings:

```json
{
  "mcpServers": {
    "toggl-focus": {
      "command": "npx",
      "args": ["-y", "toggl-focus-mcp"],
      "env": {
        "TOGGL_FOCUS_API_TOKEN": "toggl_sk_...",
        "TOGGL_ORGANIZATION_ID": "123",
        "TOGGL_WORKSPACE_ID": "456"
      }
    }
  }
}
```

Enable the server in **Cursor Settings → MCP**, then confirm tools like `focus_whoami` appear.

### Codex

CLI (recommended):

```bash
codex mcp add toggl-focus \
  --env TOGGL_FOCUS_API_TOKEN=toggl_sk_... \
  --env TOGGL_ORGANIZATION_ID=123 \
  --env TOGGL_WORKSPACE_ID=456 \
  -- npx -y toggl-focus-mcp
```

Or edit `~/.codex/config.toml` (or project `.codex/config.toml` in a trusted repo):

```toml
[mcp_servers.toggl-focus]
command = "npx"
args = ["-y", "toggl-focus-mcp"]

[mcp_servers.toggl-focus.env]
TOGGL_FOCUS_API_TOKEN = "toggl_sk_..."
TOGGL_ORGANIZATION_ID = "123"
TOGGL_WORKSPACE_ID = "456"
```

To keep secrets out of the file, set them in your shell and forward by name:

```toml
[mcp_servers.toggl-focus]
command = "npx"
args = ["-y", "toggl-focus-mcp"]
env_vars = ["TOGGL_FOCUS_API_TOKEN", "TOGGL_ORGANIZATION_ID", "TOGGL_WORKSPACE_ID"]
```

Shared across Codex CLI, IDE extension, and ChatGPT desktop Codex host. Docs: [developers.openai.com/codex/mcp](https://developers.openai.com/codex/mcp).

### OpenCode

Add to `opencode.json` / `opencode.jsonc` (project or global):

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "toggl-focus": {
      "type": "local",
      "command": ["npx", "-y", "toggl-focus-mcp"],
      "enabled": true,
      "environment": {
        "TOGGL_FOCUS_API_TOKEN": "toggl_sk_...",
        "TOGGL_ORGANIZATION_ID": "123",
        "TOGGL_WORKSPACE_ID": "456"
      }
    }
  }
}
```

You can also reference shell env vars with OpenCode’s `{env:VAR}` syntax where supported. Docs: [opencode.ai/docs/mcp-servers](https://opencode.ai/docs/mcp-servers/).

---

## Quick start prompts

Once the server is connected, try:

- “Who am I in Toggl Focus?”
- “List my projects”
- “Start a timer on project X”
- “Create a task called Ship MCP docs on project Y”
- “Show time entries for this week”

Creates, updates, deletes, and timer start/stop apply **immediately** (no confirmation token).

---

## Tools

24 tools. All are Focus/2.0 only. Shared optional args on almost every tool:

| Arg | Type | Notes |
| --- | --- | --- |
| `organization_id` | number | Overrides `TOGGL_ORGANIZATION_ID` |
| `workspace_id` | number | Overrides `TOGGL_WORKSPACE_ID` |
| `page` | number | Default `1` (list tools) |
| `per_page` | number | Default `50`, max `100` (list tools) |

### Account & context

#### `focus_whoami`

Account metadata + user settings for the authenticated Focus key.

| Arg | Type | Required | Description |
| --- | --- | --- | --- |
| `include_settings` | boolean | No | Default `true` — also fetch `GET /users/me/settings` |
| `organization_id` / `workspace_id` | number | No | Optional scope |

#### `focus_get_workspace_context`

Resolve and cache `organization_id` / `workspace_id` for later tools. Prefer setting both env vars. Does **not** call the session-only `/workspaces/{id}/context` endpoint (that returns 403 for API keys).

#### `focus_list_org_users`

List organization users (assignee lookup). Read-only.

| Arg | Type | Required | Description |
| --- | --- | --- | --- |
| `filter` | string | No | Search name/email |
| `active_status` | string | No | Active-status filter when supported |
| `page` / `per_page` | number | No | Pagination |

### Projects

#### `focus_list_projects`

List workspace projects.

| Arg | Type | Description |
| --- | --- | --- |
| `name` | string | Filter by project or client name |
| `archived` / `completed` / `pinned` | boolean | Status filters |
| `client_id` | number[] | Filter by client IDs |
| `tag_id` | number[] | Filter by tag IDs |

#### `focus_get_project`

| Arg | Type | Required |
| --- | --- | --- |
| `project_id` | number | Yes |

#### `focus_create_project`

Creates immediately. Requires `name`.

| Arg | Type | Description |
| --- | --- | --- |
| `name` | string | **Required** |
| `description` | string | |
| `client_id` | number | Supported on create |
| `color` | string | |
| `private` / `pinned` / `billable` | boolean | |
| `start_date` / `end_date` | string | `YYYY-MM-DD` |
| `tag_ids` | number[] | Applied via follow-up PATCH; may be forbidden on some plans/roles — project still creates and a `warning` is returned |
| `parent_project_id` | number | |

#### `focus_update_project`

Partial update (`PATCH`). Requires `project_id`. Same optional fields as create, plus nullable `client_id` and optional `archived`.

### Tasks

#### `focus_list_tasks`

| Arg | Type | Description |
| --- | --- | --- |
| `name` | string | Name filter |
| `project_id` | number[] | Project IDs |
| `status_id` | number[] | Status IDs |
| `assignee_user_id` | number[] | Assignees |
| `tag_id` | number[] | Tags |
| `pinned` / `archived` | boolean | |
| `parent_task_id` | number[] | Parent tasks |
| `include_assignees` | boolean | Expand assignees |

#### `focus_get_task`

Requires `task_id`. Optional `include_assignees`.

#### `focus_create_task`

Creates immediately. Requires `name`.

| Arg | Type | Description |
| --- | --- | --- |
| `name` | string | **Required** |
| `description` / `notes` | string | |
| `project_id` / `parent_task_id` / `status_id` | number | |
| `assignee_user_ids` / `tag_ids` | number[] | |
| `estimated_mins` | number | |
| `priority` | `"none"` \| `"low"` \| `"medium"` \| `"high"` | |
| `pinned` / `private` / `billable` | boolean | |
| `start_date` / `end_date` | string | `YYYY-MM-DD` |

#### `focus_update_task`

Partial update. Requires `task_id`. Same mutable fields as create (all optional).

### Live tracking

#### `focus_get_current_timer`

Returns the running timer for the workspace, or `{ "running": false }`.

#### `focus_start_timer`

Starts immediately. `type` defaults to `"activity"`.

| Arg | Type | Description |
| --- | --- | --- |
| `type` | `"activity"` \| `"break"` | Default `activity` |
| `task_id` / `project_id` | number | |
| `description` | string | |
| `billable` | boolean | |
| `tag_ids` | number[] | |
| `time_block_id` | number | |
| `start` | string | RFC3339; server default = now |

#### `focus_stop_timer`

Stops the running timer. Optional `end` (RFC3339); defaults to current UTC.

### Time entries

#### `focus_list_time_entries`

Requires a date range. Accepts `YYYY-MM-DD` or RFC3339 (`YYYY-MM-DD` is expanded to start/end of day UTC).

| Arg | Type | Required | Description |
| --- | --- | --- | --- |
| `date_from` | string | Yes | Range start |
| `date_to` | string | Yes | Range end |
| `task_id` | number | No | |
| `type` | `"activity"` \| `"break"` | No | |
| `archived` / `include_taskless` | boolean | No | |
| `time_block_id` | number | No | |

#### `focus_create_time_entry`

Creates a completed entry immediately. If `task_id` is set, uses the task-scoped endpoint; otherwise creates a taskless entry. `type` defaults to `"activity"`.

| Arg | Type | Description |
| --- | --- | --- |
| `task_id` / `project_id` | number | |
| `description` | string | |
| `start` | string | RFC3339 |
| `duration` | number | Seconds |
| `billable` | boolean | |
| `tag_ids` | number[] | |
| `time_block_id` | number | |
| `type` | `"activity"` \| `"break"` | |

#### `focus_update_time_entry`

Partial update. Requires `time_entry_id`. Optional `task_id` selects the task-scoped path.

#### `focus_delete_time_entry`

Deletes immediately. Requires `time_entry_id`. Optional `task_id` for the task-scoped path.

### Clients

#### `focus_list_clients`

Optional `name` filter plus pagination.

#### `focus_create_client`

Requires `name`. Optional `currency` (ISO 4217).

### Tags

#### `focus_list_tags`

Optional `name`, `tag_id` (array), pagination.

#### `focus_create_tag`

Requires `name` and `color` (hex or Focus color token).

### Statuses & search

#### `focus_list_statuses`

List task statuses. Optional `name`, `type`, pagination.

#### `focus_search`

Unified search across time entries, tasks, and projects.

| Arg | Type | Required | Description |
| --- | --- | --- | --- |
| `keyword` | string | Yes | Search text |
| `per_group` | number | No | Results per group |
| `project_id` / `task_id` | number | No | Scope |
| `only_me` / `include_drafts` | boolean | No | |

---

## Behavior notes

- **Mutations are immediate** — create / update / delete / start / stop have no confirmation step.
- **Project tags on create** — some Focus plans/roles reject `tag_ids` on `POST`. The tool creates the project first, then PATCHes tags; on failure you still get the project plus a `warning`.
- **Workspace context** — session cookie endpoints are not used; set org + workspace env vars.
- **Quota** — Focus enforces per-user-per-org hourly limits (e.g. Free ~30/hour). HTTP **402** means quota exhausted; wait for `X-Toggl-Quota-Resets-In`. Prefer env IDs so tools do not rediscover context every call.

---

## Development

```bash
git clone https://github.com/copperline-ai/toggl-focus-mcp.git
cd toggl-focus-mcp
npm install
npm run check          # typecheck + unit/mocked MCP tests + build (CI gate)
npm run test:live      # optional — real Focus API; local only, needs .env
node dist/index.js
```

`npm run check` is the same gate used by GitHub Actions on PRs and before publish. It uses **mocked** Focus responses — CI never calls your real Toggl account.

### Live MCP tests (local only)

Optional suite against a **real** Focus workspace. Not run in CI.

1. Copy `.env.example` → `.env` and set your Focus key + org/workspace IDs  
2. Run:

```bash
npm run test:live
```

Reuses stable fixtures (client **Toggl Focus**, project **Toggl Focus MCP**, task **MCP live test**, tag **MCP live**). Disposable time entries are deleted. Paces ~1 req/sec for rate limits. `.env` is gitignored and not published.

### Publishing

Releases use **release-it** + AI Gateway (semver bump, changelog, GitHub Release, npm publish):

```bash
npm run release:dry-run
npm pack --dry-run
```

CI secrets: `NPM_ACCESS_TOKEN`, `AI_GATEWAY_API_KEY`.

Vendored OpenAPI: [`openapi/focus.json`](openapi/focus.json) from [Focus OpenAPI](https://engineering.toggl.com/docs/focus/openapi/).

---

## Related

- Focus docs: [engineering.toggl.com/docs/focus](https://engineering.toggl.com/docs/focus/)
- Focus auth: [Bearer API keys](https://engineering.toggl.com/docs/focus/start/authentication/)
- Official OAuth MCP: [`@togglhq/mcp`](https://www.npmjs.com/package/@togglhq/mcp)

## License

MIT

TDQS

A3.5/5.0

Scored across 24 tools

Disambiguation5/5

Each tool targets a distinct resource and action (e.g., get_current_timer vs. list_time_entries, start_timer vs. create_time_entry). Even with overlapping domains, the descriptions make boundaries clear.

Naming Consistency5/5

All tools share the 'focus_' prefix and almost all follow a verb_noun pattern (list_projects, create_task, update_time_entry). The few exceptions (whoami, search) are standard single-word commands and don't break the overall consistency.

Tool Count4/5

24 tools is on the higher end but justified by the broad Toggl Focus domain covering projects, tasks, time entries, clients, tags, statuses, and user/context helpers. It feels comprehensive without redundant or trivial tools.

Completeness3/5

Core resources have strong coverage: projects and tasks support list/get/create/update, and time entries support list/create/update/delete. However, projects and tasks lack delete operations, and clients/tags only support list/create, leaving notable lifecycle gaps for these resources.

Maintenance

ActivitySlowing
ResponsivenessNo issues