@togglhq/mcp 1.11.227 → 1.12.1
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 +16 -3
- package/build/cli.js +2 -2
- package/build/index.js +1 -1
- package/build/{src-CStKKXpP.js → src-DYV7bKjL.js} +495 -114
- package/package.json +1 -1
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
|
|
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-
|
|
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-DYV7bKjL.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.
|
|
165
|
+
const packageVersion = "1.12.1";
|
|
166
166
|
let sentryEnabled = false;
|
|
167
167
|
let productAnalyticsEnabled = false;
|
|
168
168
|
runMcpCli({
|
package/build/index.js
CHANGED
|
@@ -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-
|
|
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,
|
|
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 =
|
|
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
|
|
40583
|
-
if (
|
|
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-
|
|
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.
|
|
56563
|
+
return "1.12.1";
|
|
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.
|
|
56744
|
+
const packageVersion = "1.12.1";
|
|
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
|
|
56438
|
-
const
|
|
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
|
-
|
|
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(
|
|
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-
|
|
56849
|
+
//# sourceMappingURL=src-DYV7bKjL.js.map
|