@togglhq/mcp 1.11.227 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,16 +62,29 @@ Uses **OAuth2 with PKCE** via the Toggl Accounts service. Credentials are stored
62
62
 
63
63
  ### CI or headless environments
64
64
 
65
+ Set `TOGGL_API_TOKEN` to a personal Toggl API key (`toggl_sk_…`, from Toggl 2.0 **Settings → API token**). It takes precedence over any OAuth profile: no browser opens, nothing refreshes, and nothing is written to `~/.toggl/focus-tools.json`.
66
+
65
67
  ```bash
66
68
  claude mcp add toggl-focus \
67
69
  -s user \
68
- -e TOGGL_WORKSPACE_ID=<your-workspace-id> \
69
- -e TOGGL_API_TOKEN=<your-api-token> \
70
+ -e TOGGL_API_TOKEN=<your-api-key> \
70
71
  -e TOGGL_ORGANIZATION_ID=<your-org-id> \
71
- -e TOGGL_USER_ID=<your-user-id> \
72
72
  -- npx @togglhq/mcp
73
73
  ```
74
74
 
75
+ | Variable | Required | Default when unset |
76
+ | ----------------------- | -------- | --------------------------------------------------------------------------------------------------------- |
77
+ | `TOGGL_API_TOKEN` | Yes | OAuth profile from `auth` |
78
+ | `TOGGL_ORGANIZATION_ID` | Usually | The organization last selected in Toggl's hosted MCP, if it is for the same workspace; otherwise an error |
79
+ | `TOGGL_WORKSPACE_ID` | No | Your current Toggl 2.0 workspace |
80
+ | `TOGGL_USER_ID` | No | Looked up from the API key |
81
+
82
+ API keys cannot list organizations, so set `TOGGL_ORGANIZATION_ID` unless the default above applies. App URLs read `focus.toggl.com/<organization_id>/workspaces/<workspace_id>/…`. The server checks the key and the organization/workspace pair at startup and reports a revoked key or a missing org membership by name.
83
+
84
+ API keys only reach the Toggl 2.0 API. Tools that call Toggl Accounts return `requires OAuth sign-in (run auth)` in this mode: `org-invitations`, `org-members`, `org-time-off-me`, `org-workspaces`, `shared-holidays`, `shared-status`, `shared-working-hours`, and `workspace-list` with `refresh: true`. `workspace-switch`, `profile-switch` and `profile-remove` are disabled, and `auth`, `logout` and `profile-list` report token mode instead of touching saved profiles. To use those tools, unset `TOGGL_API_TOKEN` and run `npx @togglhq/mcp auth`.
85
+
86
+ The `toggl` CLI (`@togglhq/cli`) honours the same variables; `--workspace-id` overrides `TOGGL_WORKSPACE_ID` there. The key is never logged and is redacted from error reports.
87
+
75
88
  ## Tools
76
89
 
77
90
  ### Entity tools
package/build/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as getProductAnalyticsDistinctId, c as resolvePublicSentryDsn, i as captureToolInvocation, l as shutdownProductAnalytics, n as runMcpCli, o as initProductAnalytics, r as PUBLIC_SENTRY_PRIVACY_OPTIONS, s as resolveProductAnalyticsDistinctId } from "./src-CStKKXpP.js";
2
+ import { a as getProductAnalyticsDistinctId, c as resolvePublicSentryDsn, i as captureToolInvocation, l as shutdownProductAnalytics, n as runMcpCli, o as initProductAnalytics, r as PUBLIC_SENTRY_PRIVACY_OPTIONS, s as resolveProductAnalyticsDistinctId } from "./src-Cw_5Hvfl.js";
3
3
  import * as Sentry from "@sentry/node";
4
4
  //#region src/analytics-tool-metadata.ts
5
5
  /**
@@ -162,7 +162,7 @@ function wrapServerWithProductAnalytics(server) {
162
162
  }
163
163
  //#endregion
164
164
  //#region src/cli.ts
165
- const packageVersion = "1.11.227";
165
+ const packageVersion = "1.12.0";
166
166
  let sentryEnabled = false;
167
167
  let productAnalyticsEnabled = false;
168
168
  runMcpCli({
package/build/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { n as runMcpCli, t as bootstrapServer } from "./src-CStKKXpP.js";
2
+ import { n as runMcpCli, t as bootstrapServer } from "./src-Cw_5Hvfl.js";
3
3
  export { bootstrapServer, runMcpCli };
@@ -1,11 +1,11 @@
1
1
  import { createRequire } from "node:module";
2
2
  import * as crypto$1 from "node:crypto";
3
3
  import process$1 from "node:process";
4
- import * as fs from "node:fs";
5
- import * as os from "node:os";
6
4
  import * as path from "node:path";
7
5
  import { basename, extname } from "node:path";
8
6
  import { readFile, stat, writeFile } from "node:fs/promises";
7
+ import * as fs from "node:fs";
8
+ import * as os from "node:os";
9
9
  import { createInterface } from "node:readline/promises";
10
10
  import { exec } from "node:child_process";
11
11
  import * as http from "node:http";
@@ -32484,102 +32484,6 @@ function parseAuthCliArgs(argv) {
32484
32484
  };
32485
32485
  }
32486
32486
  //#endregion
32487
- //#region ../cli-core/build/profile-Empu4JWK.js
32488
- const CONFIG_DIR = path.join(os.homedir(), ".toggl");
32489
- const CONFIG_FILE = path.join(CONFIG_DIR, "focus-tools.json");
32490
- const LEGACY_MCP_CONFIG_FILE = path.join(os.homedir(), ".toggl-focus-mcp", "config.json");
32491
- function readJsonFile(filePath) {
32492
- if (!fs.existsSync(filePath)) return null;
32493
- try {
32494
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
32495
- } catch (err) {
32496
- const msg = err instanceof Error ? err.message : String(err);
32497
- throw new Error(`Failed to read ${filePath}: ${msg}`, { cause: err });
32498
- }
32499
- }
32500
- function saveLegacyAuthConfig(config) {
32501
- const dir = path.dirname(LEGACY_MCP_CONFIG_FILE);
32502
- fs.mkdirSync(dir, { recursive: true });
32503
- const tmp = `${LEGACY_MCP_CONFIG_FILE}.${process.pid}.tmp`;
32504
- fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
32505
- fs.renameSync(tmp, LEGACY_MCP_CONFIG_FILE);
32506
- }
32507
- function loadDeveloperToolsConfig() {
32508
- const config = readJsonFile(CONFIG_FILE);
32509
- if (!config || !config.profiles) return null;
32510
- return config;
32511
- }
32512
- function loadProfile(scope = "cli", profile) {
32513
- const config = loadDeveloperToolsConfig();
32514
- if (config) {
32515
- const activeProfile = profile ?? config.active[scope];
32516
- if (activeProfile && config.profiles[activeProfile]) return config.profiles[activeProfile];
32517
- }
32518
- const legacy = readJsonFile(LEGACY_MCP_CONFIG_FILE);
32519
- if (!legacy?.access_token || !legacy.workspace_id) return null;
32520
- return legacy;
32521
- }
32522
- function saveDeveloperToolsConfig(config) {
32523
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
32524
- const tmp = `${CONFIG_FILE}.${process.pid}.tmp`;
32525
- fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
32526
- fs.renameSync(tmp, CONFIG_FILE);
32527
- }
32528
- function profileEnvironment(focusApiUrl) {
32529
- const hostname = new URL(focusApiUrl).hostname;
32530
- if (hostname === "focus.toggl.com") return "production";
32531
- return hostname.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
32532
- }
32533
- function profileSlug(value, fallback) {
32534
- return (value?.split("@")[0] ?? fallback).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase() || fallback;
32535
- }
32536
- function deriveCliProfileName(options) {
32537
- return `${profileSlug(options.email, "user")}-${options.userId}`;
32538
- }
32539
- function toProfileRow(name, auth, activeProfile) {
32540
- const workspace = auth.workspaces?.find((candidate) => candidate.workspace_id === auth.workspace_id);
32541
- return {
32542
- name,
32543
- active: name === activeProfile,
32544
- environment: profileEnvironment(auth.focus_api_url),
32545
- user_id: auth.user_id,
32546
- organization_id: auth.organization_id,
32547
- organization_name: workspace?.organization_name,
32548
- workspace_id: auth.workspace_id,
32549
- workspace_name: auth.workspace_name,
32550
- focus_api_url: auth.focus_api_url,
32551
- accounts_api_url: auth.accounts_api_url,
32552
- authenticated_at: auth.authenticated_at,
32553
- expires_at: auth.expires_at,
32554
- token_expired: auth.expires_at <= Date.now() / 1e3
32555
- };
32556
- }
32557
- function listMcpProfiles() {
32558
- const config = loadDeveloperToolsConfig();
32559
- if (!config?.profiles) return [];
32560
- return Object.entries(config.profiles).map(([name, auth]) => toProfileRow(name, auth, config.active.mcp)).sort((a, b) => a.name.localeCompare(b.name));
32561
- }
32562
- function switchMcpProfile(profileName) {
32563
- const config = loadDeveloperToolsConfig();
32564
- if (!config?.profiles?.[profileName]) throw new Error(`Profile '${profileName}' not found. Use profile-list to see profiles saved in ~/.toggl/focus-tools.json.`);
32565
- config.active.mcp = profileName;
32566
- saveDeveloperToolsConfig(config);
32567
- return toProfileRow(profileName, config.profiles[profileName], profileName);
32568
- }
32569
- function removeMcpProfile(profileName) {
32570
- const config = loadDeveloperToolsConfig();
32571
- if (!config?.profiles?.[profileName]) throw new Error(`Profile '${profileName}' not found. Use profile-list to see profiles saved in ~/.toggl/focus-tools.json.`);
32572
- const removed = toProfileRow(profileName, config.profiles[profileName], config.active.mcp);
32573
- delete config.profiles[profileName];
32574
- if (config.active.mcp === profileName) {
32575
- const next = Object.keys(config.profiles).sort()[0];
32576
- if (next) config.active.mcp = next;
32577
- else delete config.active.mcp;
32578
- }
32579
- saveDeveloperToolsConfig(config);
32580
- return removed;
32581
- }
32582
- //#endregion
32583
32487
  //#region ../cli-core/build/env-CiVJIUnv.js
32584
32488
  const PRODUCTION_URLS = {
32585
32489
  focusApiUrl: "https://focus.toggl.com",
@@ -38447,7 +38351,7 @@ async function deleteMyWorkingHours(httpClient, organization_id, data, signal) {
38447
38351
  return response.json();
38448
38352
  }
38449
38353
  //#endregion
38450
- //#region ../cli-core/build/focus-client-D9JzuIl3.js
38354
+ //#region ../cli-core/build/focus-client-CGUlmnDg.js
38451
38355
  /** Request header the API reads to attribute traffic to MCP or CLI. */
38452
38356
  const TOGGL_CLIENT_HEADER = "X-Toggl-Client";
38453
38357
  /** Header the Focus API reads for analytics and presence `platform_origin`. */
@@ -39427,7 +39331,311 @@ var FocusClient = class {
39427
39331
  }
39428
39332
  };
39429
39333
  //#endregion
39334
+ //#region ../cli-core/build/profile-BNQd2PG5.js
39335
+ const CONFIG_DIR = path.join(os.homedir(), ".toggl");
39336
+ const CONFIG_FILE = path.join(CONFIG_DIR, "focus-tools.json");
39337
+ const LEGACY_MCP_CONFIG_FILE = path.join(os.homedir(), ".toggl-focus-mcp", "config.json");
39338
+ function readJsonFile(filePath) {
39339
+ if (!fs.existsSync(filePath)) return null;
39340
+ try {
39341
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
39342
+ } catch (err) {
39343
+ const msg = err instanceof Error ? err.message : String(err);
39344
+ throw new Error(`Failed to read ${filePath}: ${msg}`, { cause: err });
39345
+ }
39346
+ }
39347
+ function saveLegacyAuthConfig(config) {
39348
+ const dir = path.dirname(LEGACY_MCP_CONFIG_FILE);
39349
+ fs.mkdirSync(dir, { recursive: true });
39350
+ const tmp = `${LEGACY_MCP_CONFIG_FILE}.${process.pid}.tmp`;
39351
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
39352
+ fs.renameSync(tmp, LEGACY_MCP_CONFIG_FILE);
39353
+ }
39354
+ function loadDeveloperToolsConfig() {
39355
+ const config = readJsonFile(CONFIG_FILE);
39356
+ if (!config || !config.profiles) return null;
39357
+ return config;
39358
+ }
39359
+ function loadProfile(scope = "cli", profile) {
39360
+ const config = loadDeveloperToolsConfig();
39361
+ if (config) {
39362
+ const activeProfile = profile ?? config.active[scope];
39363
+ if (activeProfile && config.profiles[activeProfile]) return config.profiles[activeProfile];
39364
+ }
39365
+ const legacy = readJsonFile(LEGACY_MCP_CONFIG_FILE);
39366
+ if (!legacy?.access_token || !legacy.workspace_id) return null;
39367
+ return legacy;
39368
+ }
39369
+ function saveDeveloperToolsConfig(config) {
39370
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
39371
+ const tmp = `${CONFIG_FILE}.${process.pid}.tmp`;
39372
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
39373
+ fs.renameSync(tmp, CONFIG_FILE);
39374
+ }
39375
+ function profileEnvironment(focusApiUrl) {
39376
+ const hostname = new URL(focusApiUrl).hostname;
39377
+ if (hostname === "focus.toggl.com") return "production";
39378
+ return hostname.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
39379
+ }
39380
+ function profileSlug(value, fallback) {
39381
+ return (value?.split("@")[0] ?? fallback).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase() || fallback;
39382
+ }
39383
+ function deriveCliProfileName(options) {
39384
+ return `${profileSlug(options.email, "user")}-${options.userId}`;
39385
+ }
39386
+ function toProfileRow(name, auth, activeProfile) {
39387
+ const workspace = auth.workspaces?.find((candidate) => candidate.workspace_id === auth.workspace_id);
39388
+ return {
39389
+ name,
39390
+ active: name === activeProfile,
39391
+ environment: profileEnvironment(auth.focus_api_url),
39392
+ user_id: auth.user_id,
39393
+ organization_id: auth.organization_id,
39394
+ organization_name: workspace?.organization_name,
39395
+ workspace_id: auth.workspace_id,
39396
+ workspace_name: auth.workspace_name,
39397
+ focus_api_url: auth.focus_api_url,
39398
+ accounts_api_url: auth.accounts_api_url,
39399
+ authenticated_at: auth.authenticated_at,
39400
+ expires_at: auth.expires_at,
39401
+ token_expired: auth.expires_at <= Date.now() / 1e3
39402
+ };
39403
+ }
39404
+ function listMcpProfiles() {
39405
+ const config = loadDeveloperToolsConfig();
39406
+ if (!config?.profiles) return [];
39407
+ return Object.entries(config.profiles).map(([name, auth]) => toProfileRow(name, auth, config.active.mcp)).sort((a, b) => a.name.localeCompare(b.name));
39408
+ }
39409
+ function switchMcpProfile(profileName) {
39410
+ const config = loadDeveloperToolsConfig();
39411
+ if (!config?.profiles?.[profileName]) throw new Error(`Profile '${profileName}' not found. Use profile-list to see profiles saved in ~/.toggl/focus-tools.json.`);
39412
+ config.active.mcp = profileName;
39413
+ saveDeveloperToolsConfig(config);
39414
+ return toProfileRow(profileName, config.profiles[profileName], profileName);
39415
+ }
39416
+ function removeMcpProfile(profileName) {
39417
+ const config = loadDeveloperToolsConfig();
39418
+ if (!config?.profiles?.[profileName]) throw new Error(`Profile '${profileName}' not found. Use profile-list to see profiles saved in ~/.toggl/focus-tools.json.`);
39419
+ const removed = toProfileRow(profileName, config.profiles[profileName], config.active.mcp);
39420
+ delete config.profiles[profileName];
39421
+ if (config.active.mcp === profileName) {
39422
+ const next = Object.keys(config.profiles).sort()[0];
39423
+ if (next) config.active.mcp = next;
39424
+ else delete config.active.mcp;
39425
+ }
39426
+ saveDeveloperToolsConfig(config);
39427
+ return removed;
39428
+ }
39429
+ //#endregion
39430
39430
  //#region ../cli-core/build/index.js
39431
+ /**
39432
+ * Headless auth with a personal Toggl API key (`toggl_sk_…`).
39433
+ *
39434
+ * When `TOGGL_API_TOKEN` is set it wins over any OAuth profile: no browser, no
39435
+ * refresh, and nothing is written to `~/.toggl/focus-tools.json`. The key is sent
39436
+ * to the Toggl 2.0 API as `Authorization: Bearer toggl_sk_…`. The API never falls
39437
+ * back to session auth once a key is presented, so a bad key is a hard 401.
39438
+ *
39439
+ * API-key requests carry no Accounts session. Toggl Accounts itself only accepts
39440
+ * OAuth/session credentials, so anything that calls Accounts directly (the `org`
39441
+ * and `shared` operation families) needs an OAuth sign-in instead.
39442
+ */
39443
+ const TOGGL_API_TOKEN_ENV = "TOGGL_API_TOKEN";
39444
+ const TOGGL_USER_ID_ENV = "TOGGL_USER_ID";
39445
+ const TOGGL_WORKSPACE_ID_ENV = "TOGGL_WORKSPACE_ID";
39446
+ const TOGGL_ORGANIZATION_ID_ENV = "TOGGL_ORGANIZATION_ID";
39447
+ /** Matches `apikey.KeyPrefix` in toggl/pkg. */
39448
+ const TOGGL_API_KEY_PREFIX = "toggl_sk_";
39449
+ const REDACTED_API_TOKEN = `${TOGGL_API_KEY_PREFIX}[REDACTED]`;
39450
+ var ApiTokenAuthError = class extends Error {
39451
+ status;
39452
+ constructor(message, status) {
39453
+ super(message);
39454
+ this.name = "ApiTokenAuthError";
39455
+ this.status = status;
39456
+ }
39457
+ };
39458
+ /** The raw `TOGGL_API_TOKEN` value, trimmed; blank counts as unset. */
39459
+ function readApiTokenFromEnv(env = process.env) {
39460
+ const value = env[TOGGL_API_TOKEN_ENV]?.trim();
39461
+ return value ? value : void 0;
39462
+ }
39463
+ /** True whenever `TOGGL_API_TOKEN` is set, valid or not: a bad key must fail, not fall back to OAuth. */
39464
+ function isApiTokenMode(env = process.env) {
39465
+ return readApiTokenFromEnv(env) !== void 0;
39466
+ }
39467
+ /** Returns the key, or throws without ever echoing the value back. */
39468
+ function requireApiToken(env = process.env) {
39469
+ const token = readApiTokenFromEnv(env);
39470
+ if (!token) throw new ApiTokenAuthError(`${TOGGL_API_TOKEN_ENV} is not set.`);
39471
+ if (!token.startsWith("toggl_sk_")) throw new ApiTokenAuthError(`${TOGGL_API_TOKEN_ENV} is not a Toggl API key: keys start with "${TOGGL_API_KEY_PREFIX}". Create one in Toggl 2.0 under Settings → API token.`);
39472
+ return token;
39473
+ }
39474
+ const API_KEY_PATTERN = new RegExp(`${TOGGL_API_KEY_PREFIX}[^\\s"'\`,;)}\\]]*`, "g");
39475
+ /** Replace every `toggl_sk_…` key, plus the configured token verbatim, in free text. */
39476
+ function redactApiTokens(text, env = process.env) {
39477
+ let redacted = text.replace(API_KEY_PATTERN, REDACTED_API_TOKEN);
39478
+ const token = readApiTokenFromEnv(env);
39479
+ if (token && token.length >= 8) redacted = redacted.split(token).join(REDACTED_API_TOKEN);
39480
+ return redacted;
39481
+ }
39482
+ /** Deep-redact API keys in every string of a JSON-like value, in place where possible. */
39483
+ function redactApiTokensDeep(value, env = process.env) {
39484
+ const seen = /* @__PURE__ */ new WeakSet();
39485
+ const visit = (node) => {
39486
+ if (typeof node === "string") return redactApiTokens(node, env);
39487
+ if (!node || typeof node !== "object") return node;
39488
+ if (seen.has(node)) return node;
39489
+ seen.add(node);
39490
+ if (Array.isArray(node)) {
39491
+ for (let i = 0; i < node.length; i++) node[i] = visit(node[i]);
39492
+ return node;
39493
+ }
39494
+ const record = node;
39495
+ for (const key of Object.keys(record)) record[key] = visit(record[key]);
39496
+ return node;
39497
+ };
39498
+ return visit(value);
39499
+ }
39500
+ function parseIdEnv(env, name) {
39501
+ const raw = env[name]?.trim();
39502
+ if (!raw) return void 0;
39503
+ if (!/^\d+$/.test(raw)) throw new ApiTokenAuthError(`${name} must be a positive integer.`);
39504
+ const id = Number(raw);
39505
+ if (id <= 0 || id > Number.MAX_SAFE_INTEGER) throw new ApiTokenAuthError(`${name} must be a positive integer.`);
39506
+ return id;
39507
+ }
39508
+ function positiveId(value) {
39509
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
39510
+ }
39511
+ function focusApiBase(focusApiUrl) {
39512
+ const trimmed = focusApiUrl.replace(/\/+$/, "");
39513
+ return trimmed.endsWith("/api") ? trimmed : `${trimmed}/api`;
39514
+ }
39515
+ const INVALID_KEY_MESSAGE = `Toggl rejected ${TOGGL_API_TOKEN_ENV} (401). The key may have been revoked or replaced — creating a new key invalidates the previous one. Create a fresh key in Toggl 2.0 under Settings → API token.`;
39516
+ async function getJson(url, token, options) {
39517
+ const doFetch = options.fetch ?? fetch;
39518
+ const timeoutMs = options.timeoutMs ?? 1e4;
39519
+ const origin = new URL(url).origin;
39520
+ try {
39521
+ const response = await doFetch(url, {
39522
+ headers: {
39523
+ Authorization: `Bearer ${token}`,
39524
+ Accept: "application/json",
39525
+ ...togglClientHeaders(options.clientSurface)
39526
+ },
39527
+ signal: AbortSignal.timeout(timeoutMs)
39528
+ });
39529
+ if (!response.ok) return { status: response.status };
39530
+ return {
39531
+ status: response.status,
39532
+ body: await response.json()
39533
+ };
39534
+ } catch (error) {
39535
+ if (error instanceof DOMException && error.name === "TimeoutError") throw new ApiTokenAuthError(`The Toggl 2.0 API at ${origin} did not answer within ${Math.round(timeoutMs / 1e3)}s.`);
39536
+ if (error instanceof SyntaxError) throw new ApiTokenAuthError(`The Toggl 2.0 API at ${origin} returned a response that is not JSON.`);
39537
+ throw new ApiTokenAuthError(`Could not reach the Toggl 2.0 API at ${origin}: ${redactApiTokens(error instanceof Error ? error.message : String(error), options.env)}`);
39538
+ }
39539
+ }
39540
+ /**
39541
+ * Work out the user, workspace and organization the key acts in.
39542
+ *
39543
+ * Explicit values win (`--workspace-id`, then `TOGGL_*_ID`). Otherwise:
39544
+ * - user: `GET /api/accounts/me` → `user_account_id`
39545
+ * - workspace: `GET /api/users/me/settings` → `current_workspace_id`, then the
39546
+ * workspace last picked in Toggl's hosted MCP
39547
+ * - organization: the hosted-MCP selection when it names the same workspace.
39548
+ * The Toggl 2.0 API has no "my organizations" endpoint an API key can read,
39549
+ * so any other case needs `TOGGL_ORGANIZATION_ID`.
39550
+ *
39551
+ * The settings call always runs, so a revoked key fails here rather than on the
39552
+ * first tool call. The chosen pair is then checked with one org+workspace-scoped
39553
+ * read, which is what turns a wrong or foreign organization into a clear error.
39554
+ */
39555
+ async function resolveApiTokenIdentity(options) {
39556
+ const env = options.env ?? process.env;
39557
+ const token = requireApiToken(env);
39558
+ const api = focusApiBase(options.focusApiUrl);
39559
+ const envUserId = parseIdEnv(env, TOGGL_USER_ID_ENV);
39560
+ const envWorkspaceId = parseIdEnv(env, TOGGL_WORKSPACE_ID_ENV);
39561
+ const envOrganizationId = parseIdEnv(env, TOGGL_ORGANIZATION_ID_ENV);
39562
+ const settings = await getJson(`${api}/users/me/settings`, token, options);
39563
+ if (settings.status === 401) throw new ApiTokenAuthError(INVALID_KEY_MESSAGE, 401);
39564
+ if (!settings.body) throw new ApiTokenAuthError(`Could not read your Toggl 2.0 user settings (${settings.status}).`, settings.status);
39565
+ let userId = envUserId;
39566
+ let userSource = "env";
39567
+ if (userId === void 0) {
39568
+ const me = await getJson(`${api}/accounts/me`, token, options);
39569
+ if (me.status === 401) throw new ApiTokenAuthError(INVALID_KEY_MESSAGE, 401);
39570
+ userId = positiveId(me.body?.user_account_id);
39571
+ if (userId === void 0) throw new ApiTokenAuthError(`Could not determine your Toggl user ID (${me.status}). Set ${TOGGL_USER_ID_ENV}.`, me.status);
39572
+ userSource = "api";
39573
+ }
39574
+ const hostedMcpWorkspaceId = positiveId(settings.body.mcp_active_workspace_id);
39575
+ const hostedMcpOrganizationId = positiveId(settings.body.mcp_active_organization_id);
39576
+ let workspaceId;
39577
+ let workspaceSource;
39578
+ if (options.workspaceIdOverride !== void 0) {
39579
+ workspaceId = options.workspaceIdOverride;
39580
+ workspaceSource = "flag";
39581
+ } else if (envWorkspaceId !== void 0) {
39582
+ workspaceId = envWorkspaceId;
39583
+ workspaceSource = "env";
39584
+ } else {
39585
+ workspaceId = positiveId(settings.body.current_workspace_id) ?? hostedMcpWorkspaceId;
39586
+ workspaceSource = "api";
39587
+ }
39588
+ if (workspaceId === void 0) throw new ApiTokenAuthError(`Could not determine a workspace: your Toggl 2.0 settings have no current workspace. Set ${TOGGL_WORKSPACE_ID_ENV}.`);
39589
+ let organizationId = envOrganizationId;
39590
+ let organizationSource = "env";
39591
+ if (organizationId === void 0) if (hostedMcpOrganizationId !== void 0 && hostedMcpWorkspaceId === workspaceId) {
39592
+ organizationId = hostedMcpOrganizationId;
39593
+ organizationSource = "api";
39594
+ } else throw new ApiTokenAuthError(`Could not determine the organization for workspace ${workspaceId}: API keys cannot list organizations. Set ${TOGGL_ORGANIZATION_ID_ENV} (app URLs read focus.toggl.com/<organization_id>/workspaces/<workspace_id>/…).`);
39595
+ const probe = await getJson(`${api}/organizations/${organizationId}/workspaces/${workspaceId}/custom-fields`, token, options);
39596
+ if (probe.status === 401) throw new ApiTokenAuthError(INVALID_KEY_MESSAGE, 401);
39597
+ if (probe.status === 403 || probe.status === 404) throw new ApiTokenAuthError(`This API key has no access to workspace ${workspaceId} in organization ${organizationId}. Check ${TOGGL_ORGANIZATION_ID_ENV} and ${TOGGL_WORKSPACE_ID_ENV}.`, probe.status);
39598
+ if (probe.status >= 400) throw new ApiTokenAuthError(`Could not verify access to workspace ${workspaceId} in organization ${organizationId} (${probe.status}).`, probe.status);
39599
+ return {
39600
+ user_id: userId,
39601
+ organization_id: organizationId,
39602
+ workspace_id: workspaceId,
39603
+ focus_api_url: options.focusApiUrl,
39604
+ accounts_api_url: options.accountsApiUrl,
39605
+ sources: {
39606
+ user_id: userSource,
39607
+ organization_id: organizationSource,
39608
+ workspace_id: workspaceSource
39609
+ }
39610
+ };
39611
+ }
39612
+ function createApiTokenIdentityCache(options) {
39613
+ let resolved;
39614
+ let pending;
39615
+ let failure;
39616
+ return {
39617
+ resolve() {
39618
+ if (resolved) return Promise.resolve(resolved);
39619
+ pending ??= resolveApiTokenIdentity(options()).then((identity) => {
39620
+ resolved = identity;
39621
+ failure = void 0;
39622
+ return identity;
39623
+ }).catch((error) => {
39624
+ failure = error instanceof Error ? error.message : String(error);
39625
+ throw error;
39626
+ }).finally(() => {
39627
+ pending = void 0;
39628
+ });
39629
+ return pending;
39630
+ },
39631
+ peek: () => resolved,
39632
+ lastError: () => failure
39633
+ };
39634
+ }
39635
+ /** Text every OAuth-only surface returns while `TOGGL_API_TOKEN` is set. */
39636
+ function oauthRequiredMessage(subject, authCommand) {
39637
+ return `${subject} requires OAuth sign-in (run auth): it calls Toggl Accounts, which does not accept API keys. Unset ${TOGGL_API_TOKEN_ENV} and run \`${authCommand}\` to use it.`;
39638
+ }
39431
39639
  function openAuthBrowser(url) {
39432
39640
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start \"\"" : "xdg-open";
39433
39641
  exec(`${cmd} "${url}"`, (err) => {
@@ -39995,6 +40203,7 @@ async function refreshTokens(accountsApiUrl, refreshToken, clientSurface, client
39995
40203
  return response.json();
39996
40204
  }
39997
40205
  async function getValidToken$1(scope = "cli", profile) {
40206
+ if (isApiTokenMode()) return requireApiToken();
39998
40207
  const authConfig = loadProfile(scope, profile);
39999
40208
  if (!authConfig) throw new Error(scope === "cli" ? "Not authenticated. Run `toggl auth` to sign in." : `Not authenticated. Run \`${npxMcpAuthCommand()}\` to sign in.`);
40000
40209
  const now = Date.now() / 1e3;
@@ -40202,7 +40411,7 @@ function scrubSpan(span) {
40202
40411
  /**
40203
40412
  * Last word on every event the public build sends: messages reduced to fixed
40204
40413
  * categories, no hostname, no breadcrumbs, no dependency list, no request data,
40205
- * package-relative stack paths, and no query strings on spans.
40414
+ * package-relative stack paths, no query strings on spans, and no API keys.
40206
40415
  */
40207
40416
  function scrubPublicSentryEvent(event) {
40208
40417
  redactUnsafeSentryMessages(event);
@@ -40214,7 +40423,7 @@ function scrubPublicSentryEvent(event) {
40214
40423
  for (const thread of event.threads?.values ?? []) scrubStacktrace(thread.stacktrace);
40215
40424
  for (const span of event.spans ?? []) scrubSpan(span);
40216
40425
  scrubSpan(event.contexts?.trace);
40217
- return event;
40426
+ return redactApiTokensDeep(event);
40218
40427
  }
40219
40428
  /**
40220
40429
  * Privacy options shared by `initCliTelemetry` and `initPublicSentry`.
@@ -40399,6 +40608,7 @@ function decodeAccessTokenSubject(token) {
40399
40608
  * from every other identity we already have for the same user.
40400
40609
  */
40401
40610
  function resolveProductAnalyticsDistinctId(scope) {
40611
+ if (isApiTokenMode()) return null;
40402
40612
  try {
40403
40613
  return decodeAccessTokenSubject(loadProfile(scope)?.access_token);
40404
40614
  } catch {
@@ -40517,6 +40727,35 @@ async function runAuthFlow(accountsApiUrl = "https://accounts.toggl.com", focusA
40517
40727
  };
40518
40728
  }
40519
40729
  //#endregion
40730
+ //#region src/api-token-session.ts
40731
+ let cache = null;
40732
+ /** Process-wide identity for `TOGGL_API_TOKEN`; URLs come from the env the bootstrap resolved. */
40733
+ function apiTokenIdentity() {
40734
+ cache ??= createApiTokenIdentityCache(() => {
40735
+ const urls = resolveEnvironmentUrls();
40736
+ return {
40737
+ focusApiUrl: urls.focusApiUrl,
40738
+ accountsApiUrl: urls.accountsApiUrl,
40739
+ clientSurface: "mcp"
40740
+ };
40741
+ });
40742
+ return cache;
40743
+ }
40744
+ function mcpOAuthRequiredMessage(subject) {
40745
+ return oauthRequiredMessage(subject, npxMcpAuthCommand());
40746
+ }
40747
+ /** Shape `auth`, `profile-list` and the bootstrap report for token mode. Never includes the key. */
40748
+ function describeApiTokenIdentity(identity) {
40749
+ return {
40750
+ auth_mode: "api_token",
40751
+ user_id: identity.user_id,
40752
+ organization_id: identity.organization_id,
40753
+ workspace_id: identity.workspace_id,
40754
+ focus_api_url: identity.focus_api_url,
40755
+ id_sources: identity.sources
40756
+ };
40757
+ }
40758
+ //#endregion
40520
40759
  //#region src/authenticated-client.ts
40521
40760
  const AUTH_ERROR = {
40522
40761
  ok: false,
@@ -40545,11 +40784,16 @@ function clientKey(clientConfig) {
40545
40784
  clientConfig.user_id
40546
40785
  ].join(":");
40547
40786
  }
40787
+ /** Token mode reads the resolved API-key identity; OAuth mode reads the saved profile. */
40788
+ function currentClientConfig() {
40789
+ if (isApiTokenMode()) return apiTokenIdentity().peek() ?? null;
40790
+ return loadConfig();
40791
+ }
40548
40792
  function createAuthenticatedClientProxy() {
40549
40793
  let cachedClient = null;
40550
40794
  let cachedKey = null;
40551
40795
  const resolveClient = () => {
40552
- const config = loadConfig();
40796
+ const config = currentClientConfig();
40553
40797
  if (!config) return null;
40554
40798
  const key = clientKey(config);
40555
40799
  if (cachedClient && cachedKey === key) return cachedClient;
@@ -40569,6 +40813,20 @@ function createAuthenticatedClientProxy() {
40569
40813
  cachedKey = key;
40570
40814
  return cachedClient;
40571
40815
  };
40816
+ /** Token mode resolves the identity lazily (and retries after a failure) before the first call. */
40817
+ const resolveClientForCall = async () => {
40818
+ if (isApiTokenMode() && !apiTokenIdentity().peek()) try {
40819
+ await apiTokenIdentity().resolve();
40820
+ } catch (error) {
40821
+ return { error: {
40822
+ ok: false,
40823
+ status: 401,
40824
+ message: `TOGGL_API_TOKEN sign-in failed: ${error instanceof Error ? error.message : String(error)}`
40825
+ } };
40826
+ }
40827
+ const client = resolveClient();
40828
+ return client ? { client } : { error: AUTH_ERROR };
40829
+ };
40572
40830
  return new Proxy({}, { get(_target, prop, receiver) {
40573
40831
  if (prop === "userId") return resolveClient()?.userId ?? 0;
40574
40832
  if (prop === "workspaceId") return resolveClient()?.workspaceId ?? "";
@@ -40579,8 +40837,9 @@ function createAuthenticatedClientProxy() {
40579
40837
  }
40580
40838
  if (FOCUS_CLIENT_SYNC_PASSTHROUGH_PROP_KEYS.includes(prop)) return readFocusClientSyncPassthroughProp(resolveClient(), prop);
40581
40839
  return async (...args) => {
40582
- const client = resolveClient();
40583
- if (!client) return AUTH_ERROR;
40840
+ const resolved = await resolveClientForCall();
40841
+ if ("error" in resolved) return resolved.error;
40842
+ const { client } = resolved;
40584
40843
  const method = client[prop];
40585
40844
  if (typeof method !== "function") return {
40586
40845
  ok: false,
@@ -40592,7 +40851,7 @@ function createAuthenticatedClientProxy() {
40592
40851
  } });
40593
40852
  }
40594
40853
  //#endregion
40595
- //#region ../operations/build/validation-Byte2z0O.js
40854
+ //#region ../operations/build/validation-Cl22AQf7.js
40596
40855
  const optionalNumber = /* @__PURE__ */ optional$1(/* @__PURE__ */ number$2());
40597
40856
  const perPageField = (max) => /* @__PURE__ */ optional$1(/* @__PURE__ */ pipe$2(/* @__PURE__ */ number$2(), /* @__PURE__ */ integer(), /* @__PURE__ */ minValue$1(1), /* @__PURE__ */ maxValue$1(max), /* @__PURE__ */ description$1(`Results per page (1-${max})`)));
40598
40857
  const perPageSchema = perPageField(100);
@@ -41988,6 +42247,14 @@ const orgSharedOperations = [
41988
42247
  function operationRequiresConfirmation(operation) {
41989
42248
  return operation.destructive || operation.safety !== "read";
41990
42249
  }
42250
+ /**
42251
+ * `org` and `shared` operations call Toggl Accounts directly, and Accounts only
42252
+ * accepts OAuth/session credentials. With `TOGGL_API_TOKEN` they cannot work, so
42253
+ * both surfaces short-circuit them with a sign-in message instead of a 401.
42254
+ */
42255
+ function operationRequiresOAuth(operation) {
42256
+ return operation.family === "org" || operation.family === "shared";
42257
+ }
41991
42258
  const MAX_ERROR_LENGTH = 200;
41992
42259
  const MAX_RECEIVED_LENGTH = 40;
41993
42260
  function formatIssueMessage(issue) {
@@ -55186,6 +55453,16 @@ function buildToolInventoryMarkdown() {
55186
55453
  const record = buildToolInventoryRecord();
55187
55454
  return Object.entries(record).map(([tool, actions]) => `- **${tool}** — actions: ${actions.join(", ")}`).join("\n");
55188
55455
  }
55456
+ /** Entity tools whose every action calls Toggl Accounts, so they need OAuth even when a key is set. */
55457
+ function oauthOnlyEntityTools() {
55458
+ return entityToolNames().filter((tool) => operationsByEntityTool(tool).every(operationRequiresOAuth)).sort((a, b) => a.localeCompare(b));
55459
+ }
55460
+ function apiTokenSection() {
55461
+ return `## API token mode
55462
+
55463
+ This server authenticates with TOGGL_API_TOKEN. These tools need an OAuth sign-in and return "requires OAuth sign-in (run auth)" instead: ${oauthOnlyEntityTools().join(", ")}, plus \`workspace-list\` with \`refresh: true\`. Workspace and profile switching are disabled; the workspace comes from TOGGL_WORKSPACE_ID / TOGGL_ORGANIZATION_ID.
55464
+ `;
55465
+ }
55189
55466
  function buildMcpServerInstructions(options) {
55190
55467
  const core = `## Entity tools
55191
55468
 
@@ -55210,6 +55487,10 @@ Entity mutations and \`profile-remove\` issue a \`confirm_token\` on the first c
55210
55487
  - Task priority levels when setting priority: \`none\`, \`low\`, \`medium\`, \`high\`.
55211
55488
  - Time entries are tracked time.
55212
55489
  `;
55490
+ if (options.authMode === "api_token") return `${options.authenticated ? `You are connected to the Toggl 2.0 ${options.workspaceLabel ?? "workspace"}.` : `Toggl 2.0 MCP could not sign in with TOGGL_API_TOKEN: ${options.apiTokenError ?? "unknown error"}`}
55491
+
55492
+ ${apiTokenSection()}
55493
+ ${core}`;
55213
55494
  if (options.authenticated) return `You are connected to the Toggl 2.0 workspace ${options.workspaceLabel ?? "your workspace"}.
55214
55495
 
55215
55496
  ${core}`;
@@ -55955,8 +56236,34 @@ const profileRemoveInput = /* @__PURE__ */ object({
55955
56236
  profile_name: profileNameField,
55956
56237
  confirm_token: optionalConfirmToken
55957
56238
  });
56239
+ const API_TOKEN_PROFILES_MESSAGE = "Saved profiles are not used while TOGGL_API_TOKEN is set: every call authenticates with the API key. Remove TOGGL_API_TOKEN from the MCP server config to use OAuth profiles again.";
56240
+ const API_TOKEN_WORKSPACE_MESSAGE = "While TOGGL_API_TOKEN is set the workspace comes from TOGGL_WORKSPACE_ID and TOGGL_ORGANIZATION_ID (or your Toggl 2.0 current workspace). Change them and restart the MCP server to switch.";
56241
+ async function apiTokenStatus() {
56242
+ try {
56243
+ return {
56244
+ ok: true,
56245
+ identity: await apiTokenIdentity().resolve()
56246
+ };
56247
+ } catch (error) {
56248
+ return {
56249
+ ok: false,
56250
+ message: `TOGGL_API_TOKEN sign-in failed: ${error instanceof Error ? error.message : String(error)}`
56251
+ };
56252
+ }
56253
+ }
55958
56254
  function registerWorkspaceTools(server) {
56255
+ const runApiTokenAuth = async () => {
56256
+ const status = await apiTokenStatus();
56257
+ if (!status.ok) return mcpFailure(status.message);
56258
+ return mcpText(JSON.stringify({
56259
+ authenticated: true,
56260
+ requires_workspace_selection: false,
56261
+ ...describeApiTokenIdentity(status.identity),
56262
+ message: "Signed in with TOGGL_API_TOKEN. No browser sign-in runs and nothing is saved to ~/.toggl/focus-tools.json."
56263
+ }, null, 2));
56264
+ };
55959
56265
  const runAuth = async ({ workspace_id, accounts_api_url, focus_api_url }) => {
56266
+ if (isApiTokenMode()) return runApiTokenAuth();
55960
56267
  try {
55961
56268
  const continuation = pendingAuthContext && workspace_id !== void 0 ? pendingAuthContext : void 0;
55962
56269
  const authResult = await runAuthFlow(accounts_api_url ?? process.env.TOGGL_ACCOUNTS_API_URL, focus_api_url ?? process.env.TOGGL_FOCUS_API_URL, {
@@ -56011,6 +56318,12 @@ function registerWorkspaceTools(server) {
56011
56318
  };
56012
56319
  const runLogout = async () => {
56013
56320
  pendingAuthContext = null;
56321
+ if (isApiTokenMode()) return mcpText(JSON.stringify({
56322
+ logged_out: false,
56323
+ auth_mode: "api_token",
56324
+ removed_config: false,
56325
+ message: "This server authenticates with TOGGL_API_TOKEN, so there is no stored session to clear. Remove TOGGL_API_TOKEN from the MCP server config to sign out. Saved OAuth profiles were left untouched."
56326
+ }, null, 2));
56014
56327
  const removed = clearConfig();
56015
56328
  return { content: [{
56016
56329
  type: "text",
@@ -56032,6 +56345,17 @@ function registerWorkspaceTools(server) {
56032
56345
  },
56033
56346
  inputSchema: toolObject(workspaceListInput)
56034
56347
  }, async (params, _ctx) => {
56348
+ if (isApiTokenMode()) {
56349
+ if (params.refresh) return mcpFailure(mcpOAuthRequiredMessage("workspace-list refresh"));
56350
+ const status = await apiTokenStatus();
56351
+ if (!status.ok) return mcpFailure(status.message);
56352
+ return mcpText(JSON.stringify([{
56353
+ workspace_id: status.identity.workspace_id,
56354
+ organization_id: status.identity.organization_id,
56355
+ active: true,
56356
+ auth_mode: "api_token"
56357
+ }], null, 2));
56358
+ }
56035
56359
  try {
56036
56360
  if (params.refresh) await refreshMcpWorkspaces();
56037
56361
  } catch (error) {
@@ -56078,6 +56402,7 @@ function registerWorkspaceTools(server) {
56078
56402
  },
56079
56403
  inputSchema: toolObject(workspaceSwitchToolInput)
56080
56404
  }, async (params, _ctx) => {
56405
+ if (isApiTokenMode()) return mcpFailure(API_TOKEN_WORKSPACE_MESSAGE);
56081
56406
  const { workspace_id } = params;
56082
56407
  const config = loadConfig();
56083
56408
  if (!config) return {
@@ -56114,6 +56439,14 @@ function registerWorkspaceTools(server) {
56114
56439
  openWorldHint: false
56115
56440
  }
56116
56441
  }, async (_ctx) => {
56442
+ if (isApiTokenMode()) {
56443
+ const status = await apiTokenStatus();
56444
+ return mcpText(JSON.stringify({
56445
+ auth_mode: "api_token",
56446
+ active: status.ok ? describeApiTokenIdentity(status.identity) : { error: status.message },
56447
+ message: API_TOKEN_PROFILES_MESSAGE
56448
+ }, null, 2));
56449
+ }
56117
56450
  const payload = listMcpProfiles().map((row) => ({
56118
56451
  name: row.name,
56119
56452
  active_for_mcp: row.active,
@@ -56141,6 +56474,7 @@ function registerWorkspaceTools(server) {
56141
56474
  },
56142
56475
  inputSchema: toolObject(profileNameInput)
56143
56476
  }, async (params, _ctx) => {
56477
+ if (isApiTokenMode()) return mcpFailure(API_TOKEN_PROFILES_MESSAGE);
56144
56478
  try {
56145
56479
  const row = switchMcpProfile(params.profile_name);
56146
56480
  return { content: [{
@@ -56173,6 +56507,7 @@ function registerWorkspaceTools(server) {
56173
56507
  },
56174
56508
  inputSchema: toolObject(profileRemoveInput)
56175
56509
  }, async (params, _ctx) => {
56510
+ if (isApiTokenMode()) return mcpFailure(API_TOKEN_PROFILES_MESSAGE);
56176
56511
  return withMutationApproval({ confirm_token: params.confirm_token }, async (approvalScope) => {
56177
56512
  approvalScope.requireMutationApproval("profile-remove", mutationFingerprint({ profile_name: params.profile_name }), () => JSON.stringify({ profile_name: params.profile_name }));
56178
56513
  try {
@@ -56197,7 +56532,7 @@ function registerWorkspaceTools(server) {
56197
56532
  });
56198
56533
  server.registerTool("auth", {
56199
56534
  title: "Authenticate MCP",
56200
- description: "Authenticate this MCP server with Toggl 2.0 via OAuth. Opens browser login and stores credentials locally. Optionally pass workspace_id to pick a specific workspace when multiple are available.",
56535
+ description: "Authenticate this MCP server with Toggl 2.0 via OAuth. Opens browser login and stores credentials locally. Optionally pass workspace_id to pick a specific workspace when multiple are available. When the server runs with TOGGL_API_TOKEN, reports the API-key identity instead and never opens a browser.",
56201
56536
  annotations: {
56202
56537
  readOnlyHint: false,
56203
56538
  destructiveHint: false,
@@ -56225,7 +56560,7 @@ function registerWorkspaceTools(server) {
56225
56560
  //#region src/package-version.ts
56226
56561
  function mcpPackageVersion() {
56227
56562
  try {
56228
- return "1.11.227";
56563
+ return "1.12.0";
56229
56564
  } catch {
56230
56565
  return "unknown";
56231
56566
  }
@@ -56340,6 +56675,7 @@ async function runOperation(operation, data, client, confirmToken, dryRun) {
56340
56675
  status: 403,
56341
56676
  message: `Operation '${operation.id}' is not available in this build.`
56342
56677
  });
56678
+ if (isApiTokenMode() && operationRequiresOAuth(operation)) return mcpFailure(mcpOAuthRequiredMessage(`${operation.mcp?.entityTool ?? operation.id} action '${operation.action}'`));
56343
56679
  const parseResult = /* @__PURE__ */ safeParse(operation.inputSchema, coercePayload(data) ?? {});
56344
56680
  if (!parseResult.success) return mcpFailure(`Invalid input: ${formatValibotError(parseResult.issues)}`);
56345
56681
  const input = parseResult.output;
@@ -56348,6 +56684,11 @@ async function runOperation(operation, data, client, confirmToken, dryRun) {
56348
56684
  operation: operation.id,
56349
56685
  input
56350
56686
  }, null, 2));
56687
+ if (isApiTokenMode()) try {
56688
+ await apiTokenIdentity().resolve();
56689
+ } catch (error) {
56690
+ return mcpFailure(`TOGGL_API_TOKEN sign-in failed: ${error instanceof Error ? error.message : String(error)}`);
56691
+ }
56351
56692
  if (!operationRequiresConfirmation(operation)) return apiResult(await executeDecorated(operation, input, client));
56352
56693
  return withMutationApproval({ confirm_token: confirmToken }, async (approvalScope) => {
56353
56694
  approvalScope.requireMutationApproval(operation.id, mutationFingerprint(input), () => JSON.stringify({
@@ -56400,7 +56741,7 @@ import_main.default.config({
56400
56741
  quiet: true,
56401
56742
  ignore: ["MISSING_ENV_FILE"]
56402
56743
  });
56403
- const packageVersion = "1.11.227";
56744
+ const packageVersion = "1.12.0";
56404
56745
  function resolvePublicUrls() {
56405
56746
  return {
56406
56747
  focusApiUrl: process.env.TOGGL_FOCUS_API_URL ?? "https://focus.toggl.com",
@@ -56417,7 +56758,35 @@ function runMcpCli(options = {}) {
56417
56758
  if (process.argv[2] === "auth") runAuthCommand(urls);
56418
56759
  else bootstrapServer(options);
56419
56760
  }
56761
+ /** MCP clients abandon `initialize` after a few seconds, so startup never waits on every probe. */
56762
+ const API_TOKEN_STARTUP_BUDGET_MS = 1e4;
56763
+ /**
56764
+ * Bounds how long startup waits for the API-key identity. Past the budget the server
56765
+ * starts anyway; the cache keeps resolving and tool calls pick up its result.
56766
+ */
56767
+ function resolveApiTokenWithinStartupBudget() {
56768
+ let timer;
56769
+ const budget = new Promise((_resolve, reject) => {
56770
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`the Toggl 2.0 API had not answered after ${API_TOKEN_STARTUP_BUDGET_MS / 1e3}s at startup; tools retry the check on first use.`)), API_TOKEN_STARTUP_BUDGET_MS);
56771
+ timer.unref();
56772
+ });
56773
+ return Promise.race([apiTokenIdentity().resolve(), budget]).finally(() => clearTimeout(timer));
56774
+ }
56775
+ async function runApiTokenAuthCommand() {
56776
+ try {
56777
+ const identity = await apiTokenIdentity().resolve();
56778
+ console.error("TOGGL_API_TOKEN is set, so no browser sign-in runs and nothing is saved to ~/.toggl/focus-tools.json.");
56779
+ console.error(JSON.stringify(describeApiTokenIdentity(identity), null, 2));
56780
+ } catch (err) {
56781
+ console.error("Auth failed:", err instanceof Error ? err.message : err);
56782
+ process.exit(1);
56783
+ }
56784
+ }
56420
56785
  function runAuthCommand(urls) {
56786
+ if (isApiTokenMode()) {
56787
+ runApiTokenAuthCommand();
56788
+ return;
56789
+ }
56421
56790
  let authOptions;
56422
56791
  try {
56423
56792
  authOptions = parseAuthCliArgs(process.argv.slice(3));
@@ -56434,19 +56803,31 @@ function runAuthCommand(urls) {
56434
56803
  function bootstrapServer(options = {}) {
56435
56804
  const urls = options.resolveUrls?.() ?? resolvePublicUrls();
56436
56805
  applyResolvedUrls(urls);
56437
- const config = loadConfig();
56438
- const authenticated = Boolean(config);
56806
+ const tokenMode = isApiTokenMode();
56807
+ const config = tokenMode ? null : loadConfig();
56808
+ let authenticated = Boolean(config);
56439
56809
  console.error(`Using Toggl 2.0 API: ${urls.focusApiUrl}`);
56440
56810
  const client = createAuthenticatedClientProxy();
56441
- const workspaceLabel = config ? `${config.workspace_name ?? "Workspace"} (ID: ${config.workspace_id})` : void 0;
56811
+ let workspaceLabel = config ? `${config.workspace_name ?? "Workspace"} (ID: ${config.workspace_id})` : void 0;
56812
+ let apiTokenError;
56442
56813
  async function main() {
56814
+ if (tokenMode) try {
56815
+ const identity = await resolveApiTokenWithinStartupBudget();
56816
+ authenticated = true;
56817
+ workspaceLabel = `workspace ${identity.workspace_id} in organization ${identity.organization_id} (TOGGL_API_TOKEN)`;
56818
+ } catch (err) {
56819
+ apiTokenError = err instanceof Error ? err.message : String(err);
56820
+ console.error(`TOGGL_API_TOKEN sign-in failed: ${apiTokenError}`);
56821
+ }
56443
56822
  serveStdio(() => {
56444
56823
  const rawServer = new McpServer({
56445
56824
  name: "toggl-focus",
56446
56825
  version: packageVersion
56447
56826
  }, { instructions: buildMcpServerInstructions({
56448
56827
  authenticated,
56449
- workspaceLabel
56828
+ workspaceLabel,
56829
+ authMode: tokenMode ? "api_token" : "oauth",
56830
+ apiTokenError
56450
56831
  }) });
56451
56832
  const server = options.beforeRegister?.(rawServer) ?? rawServer;
56452
56833
  registerWorkspaceTools(server);
@@ -56454,7 +56835,7 @@ function bootstrapServer(options = {}) {
56454
56835
  options.beforeConnect?.();
56455
56836
  return server;
56456
56837
  }, { legacy: "serve" });
56457
- console.error(config ? `Toggl 2.0 MCP server running on stdio (workspace: ${workspaceLabel})` : "Toggl 2.0 MCP server running on stdio (not authenticated)");
56838
+ console.error(authenticated ? `Toggl 2.0 MCP server running on stdio (workspace: ${workspaceLabel})` : "Toggl 2.0 MCP server running on stdio (not authenticated)");
56458
56839
  }
56459
56840
  main().catch((err) => {
56460
56841
  console.error("Fatal error:", err);
@@ -56465,4 +56846,4 @@ function bootstrapServer(options = {}) {
56465
56846
  //#endregion
56466
56847
  export { getProductAnalyticsDistinctId as a, resolvePublicSentryDsn as c, captureToolInvocation as i, shutdownProductAnalytics as l, runMcpCli as n, initProductAnalytics as o, PUBLIC_SENTRY_PRIVACY_OPTIONS as r, resolveProductAnalyticsDistinctId as s, bootstrapServer as t };
56467
56848
 
56468
- //# sourceMappingURL=src-CStKKXpP.js.map
56849
+ //# sourceMappingURL=src-Cw_5Hvfl.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@togglhq/mcp",
3
- "version": "1.11.227",
3
+ "version": "1.12.0",
4
4
  "description": "Toggl 2.0 MCP server for Claude Code, Claude Desktop, and other MCP clients.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",