@togglhq/mcp 1.13.140 → 1.13.141

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
@@ -100,6 +100,10 @@ One tool per domain (for example `tasks`, `projects`, `time-entries`, `organizat
100
100
 
101
101
  **Mutation confirmation:** mutating tools return `confirm_required` with a `confirm_token` on the first call; repeat with `confirm_token` to execute.
102
102
 
103
+ ### Deep links
104
+
105
+ - **deep-links** — read-only, no API calls. `action: "get"` returns the Toggl 2.0 web URL for a `task`, `project`, `time_entry`, `calendar_day`, `report` or `member` in the active organization and workspace, on the profile's environment (production or staging). It also returns a Track URL where the IDs map (members, by organization user ID); otherwise `track.url` is `null` with a `note` saying why. A time entry needs `task_id` to open directly; `calendar_day` opens the calendar on the week of its `date`. Example: `{ "action": "get", "data": { "entity": "report", "report_type": "detailed", "period": "prevMonth" } }`.
106
+
103
107
  ### Workspace, profiles, and auth
104
108
 
105
109
  - **workspace-list** — list workspaces (`refresh: true` refetches from Accounts)
package/build/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as PUBLIC_SENTRY_PRIVACY_OPTIONS, c as initProductAnalytics, d as shutdownProductAnalytics, i as runMcpCli, l as resolveProductAnalyticsDistinctId, o as captureToolInvocation, s as getProductAnalyticsDistinctId, u as resolvePublicSentryDsn } from "./src-D5fshNIt.js";
2
+ import { a as PUBLIC_SENTRY_PRIVACY_OPTIONS, c as initProductAnalytics, d as shutdownProductAnalytics, i as runMcpCli, l as resolveProductAnalyticsDistinctId, o as captureToolInvocation, s as getProductAnalyticsDistinctId, u as resolvePublicSentryDsn } from "./src-ZNZfSwsi.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.13.140";
165
+ const packageVersion = "1.13.141";
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 { i as runMcpCli, n as MCP_MAX_TOOL_INPUT_ELEMENTS, r as bootstrapServer, t as MCP_CACHE_TTL_MS } from "./src-D5fshNIt.js";
2
+ import { i as runMcpCli, n as MCP_MAX_TOOL_INPUT_ELEMENTS, r as bootstrapServer, t as MCP_CACHE_TTL_MS } from "./src-ZNZfSwsi.js";
3
3
  export { MCP_CACHE_TTL_MS, MCP_MAX_TOOL_INPUT_ELEMENTS, bootstrapServer, runMcpCli };
@@ -38671,7 +38671,7 @@ async function deleteMyWorkingHours(httpClient, organization_id, data, signal) {
38671
38671
  return response.json();
38672
38672
  }
38673
38673
  //#endregion
38674
- //#region ../cli-core/build/focus-client-CGUlmnDg.js
38674
+ //#region ../cli-core/build/focus-client-CGpMYUFy.js
38675
38675
  /** Request header the API reads to attribute traffic to MCP or CLI. */
38676
38676
  const TOGGL_CLIENT_HEADER = "X-Toggl-Client";
38677
38677
  /** Header the Focus API reads for analytics and presence `platform_origin`. */
@@ -38908,9 +38908,12 @@ var FocusClient = class {
38908
38908
  organizationIdNum;
38909
38909
  workspaceId;
38910
38910
  workspaceIdNum;
38911
+ /** Focus origin without the `/api` suffix, e.g. `https://focus.toggl.com`. */
38912
+ focusBaseUrl;
38911
38913
  constructor(config) {
38912
38914
  let baseUrl = config.baseUrl.replace(/\/+$/, "");
38913
38915
  if (!baseUrl.endsWith("/api")) baseUrl += "/api";
38916
+ this.focusBaseUrl = baseUrl.slice(0, -4);
38914
38917
  this.tokenProvider = config.tokenProvider;
38915
38918
  this.clientSurface = config.clientSurface;
38916
38919
  this.userId = config.userId;
@@ -39825,7 +39828,7 @@ function parseIdEnv(env, name) {
39825
39828
  if (id <= 0 || id > Number.MAX_SAFE_INTEGER) throw new ApiTokenAuthError(`${name} must be a positive integer.`);
39826
39829
  return id;
39827
39830
  }
39828
- function positiveId(value) {
39831
+ function positiveId$1(value) {
39829
39832
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
39830
39833
  }
39831
39834
  function focusApiBase(focusApiUrl) {
@@ -39887,12 +39890,12 @@ async function resolveApiTokenIdentity(options) {
39887
39890
  if (userId === void 0) {
39888
39891
  const me = await getJson(`${api}/accounts/me`, token, options);
39889
39892
  if (me.status === 401) throw new ApiTokenAuthError(INVALID_KEY_MESSAGE, 401);
39890
- userId = positiveId(me.body?.user_account_id);
39893
+ userId = positiveId$1(me.body?.user_account_id);
39891
39894
  if (userId === void 0) throw new ApiTokenAuthError(`Could not determine your Toggl user ID (${me.status}). Set ${TOGGL_USER_ID_ENV}.`, me.status);
39892
39895
  userSource = "api";
39893
39896
  }
39894
- const hostedMcpWorkspaceId = positiveId(settings.body.mcp_active_workspace_id);
39895
- const hostedMcpOrganizationId = positiveId(settings.body.mcp_active_organization_id);
39897
+ const hostedMcpWorkspaceId = positiveId$1(settings.body.mcp_active_workspace_id);
39898
+ const hostedMcpOrganizationId = positiveId$1(settings.body.mcp_active_organization_id);
39896
39899
  let workspaceId;
39897
39900
  let workspaceSource;
39898
39901
  if (options.workspaceIdOverride !== void 0) {
@@ -39902,7 +39905,7 @@ async function resolveApiTokenIdentity(options) {
39902
39905
  workspaceId = envWorkspaceId;
39903
39906
  workspaceSource = "env";
39904
39907
  } else {
39905
- workspaceId = positiveId(settings.body.current_workspace_id) ?? hostedMcpWorkspaceId;
39908
+ workspaceId = positiveId$1(settings.body.current_workspace_id) ?? hostedMcpWorkspaceId;
39906
39909
  workspaceSource = "api";
39907
39910
  }
39908
39911
  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}.`);
@@ -41085,7 +41088,8 @@ const AUTH_ERROR = {
41085
41088
  const FOCUS_CLIENT_SYNC_PASSTHROUGH_PROP_KEYS = [
41086
41089
  "httpClient",
41087
41090
  "organizationId",
41088
- "workspaceIdNumber"
41091
+ "workspaceIdNumber",
41092
+ "focusBaseUrl"
41089
41093
  ];
41090
41094
  function readFocusClientSyncPassthroughProp(client, prop) {
41091
41095
  if (!client) return;
@@ -41171,7 +41175,7 @@ function createAuthenticatedClientProxy() {
41171
41175
  } });
41172
41176
  }
41173
41177
  //#endregion
41174
- //#region ../operations/build/validation-Cl22AQf7.js
41178
+ //#region ../operations/build/validation-CQfYPZ9x.js
41175
41179
  const optionalNumber = /* @__PURE__ */ optional$1(/* @__PURE__ */ number$2());
41176
41180
  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})`)));
41177
41181
  const perPageSchema = perPageField(100);
@@ -42209,6 +42213,226 @@ const versionOverrides = { "focus.version.service": {
42209
42213
  description: "MCP package version plus the Focus API service revision.",
42210
42214
  agentGuidance: "mcp.package is this published MCP package version (e.g. 1.8.0). focus_api is the Focus API process revision (git SHA / build number) — it does not change when you update the npm MCP package."
42211
42215
  } };
42216
+ const DEEP_LINK_ENTITIES = [
42217
+ "task",
42218
+ "project",
42219
+ "time_entry",
42220
+ "calendar_day",
42221
+ "report",
42222
+ "member"
42223
+ ];
42224
+ const DEEP_LINK_REPORT_TYPES = [
42225
+ "summary",
42226
+ "detailed",
42227
+ "profitability",
42228
+ "utilization",
42229
+ "workload",
42230
+ "time-accounting",
42231
+ "time-off"
42232
+ ];
42233
+ const DEEP_LINK_REPORT_PERIODS = [
42234
+ "today",
42235
+ "yesterday",
42236
+ "thisWeek",
42237
+ "nextWeek",
42238
+ "weekToDate",
42239
+ "prevWeek",
42240
+ "last2Weeks",
42241
+ "thisMonth",
42242
+ "nextMonth",
42243
+ "prevMonth",
42244
+ "monthToDate",
42245
+ "thisQuarter",
42246
+ "nextQuarter",
42247
+ "quarterToDate",
42248
+ "thisSemester",
42249
+ "semesterToDate",
42250
+ "last30Days",
42251
+ "last90Days",
42252
+ "lastQuarter",
42253
+ "lastSemester",
42254
+ "last12Months",
42255
+ "thisYear",
42256
+ "yearToDate",
42257
+ "prevYear",
42258
+ "allTime"
42259
+ ];
42260
+ const PROJECT_TABS = [
42261
+ "tasks",
42262
+ "board",
42263
+ "timeline",
42264
+ "dashboard",
42265
+ "members",
42266
+ "period"
42267
+ ];
42268
+ const MEMBER_TABS = ["time-off"];
42269
+ const DEEP_LINK_TABS = [...PROJECT_TABS, ...MEMBER_TABS];
42270
+ const PERIOD_REPORT_TYPES = new Set(DEEP_LINK_REPORT_TYPES.filter((type) => type !== "time-off"));
42271
+ const positiveId = /* @__PURE__ */ pipe$2(/* @__PURE__ */ number$2(), /* @__PURE__ */ integer(), /* @__PURE__ */ minValue$1(1));
42272
+ const isoDay = /* @__PURE__ */ pipe$2(/* @__PURE__ */ string$1(), /* @__PURE__ */ isoDate$1("must be YYYY-MM-DD"));
42273
+ const DeepLinkInputSchema = /* @__PURE__ */ object$2({
42274
+ entity: /* @__PURE__ */ picklist$1(DEEP_LINK_ENTITIES),
42275
+ id: /* @__PURE__ */ optional$1(positiveId),
42276
+ task_id: /* @__PURE__ */ optional$1(positiveId),
42277
+ project_id: /* @__PURE__ */ optional$1(positiveId),
42278
+ date: /* @__PURE__ */ optional$1(isoDay),
42279
+ report_type: /* @__PURE__ */ optional$1(/* @__PURE__ */ picklist$1(DEEP_LINK_REPORT_TYPES)),
42280
+ period: /* @__PURE__ */ optional$1(/* @__PURE__ */ picklist$1(DEEP_LINK_REPORT_PERIODS)),
42281
+ from: /* @__PURE__ */ optional$1(isoDay),
42282
+ to: /* @__PURE__ */ optional$1(isoDay),
42283
+ tab: /* @__PURE__ */ optional$1(/* @__PURE__ */ picklist$1(DEEP_LINK_TABS))
42284
+ });
42285
+ const ENTITIES_WITH_ID = new Set([
42286
+ "task",
42287
+ "project",
42288
+ "time_entry",
42289
+ "member"
42290
+ ]);
42291
+ /** Cross-field rules the schema cannot carry; returns the first violation. */
42292
+ function deepLinkInputError(input) {
42293
+ if (input.id === void 0 && ENTITIES_WITH_ID.has(input.entity)) return "id is required for task, project, time_entry and member (member id is the organization user ID)";
42294
+ if (input.tab !== void 0 && !(input.entity === "project" && PROJECT_TABS.includes(input.tab)) && !(input.entity === "member" && MEMBER_TABS.includes(input.tab))) return `tab is ${PROJECT_TABS.join("|")} for project, or ${MEMBER_TABS.join("|")} for member`;
42295
+ if (input.from === void 0 !== (input.to === void 0)) return "from and to must be passed together";
42296
+ if (input.period !== void 0 && input.from !== void 0) return "pass either period or from/to, not both";
42297
+ if ((input.period !== void 0 || input.from !== void 0) && !(input.entity === "report" && PERIOD_REPORT_TYPES.has(input.report_type ?? "summary"))) return "period and from/to apply only to report entities other than time-off";
42298
+ if (input.from !== void 0 && input.to !== void 0 && input.from > input.to) return "from must not be after to";
42299
+ }
42300
+ const FOCUS_HOST_PATTERN = /^focus\.((?:[a-z0-9-]+\.)*toggl\.(?:com|space))$/i;
42301
+ const APP_HOST_PATTERN = /^app\.(toggl\.(?:com|space)|[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.ms\.toggl\.space)$/i;
42302
+ function trackBaseUrlFor(focusBaseUrl) {
42303
+ const hostname = new URL(focusBaseUrl).hostname.toLowerCase();
42304
+ const base = FOCUS_HOST_PATTERN.exec(hostname)?.[1] ?? APP_HOST_PATTERN.exec(hostname)?.[1];
42305
+ return base ? `https://track.${base}` : null;
42306
+ }
42307
+ function withQuery(url, params) {
42308
+ const search = new URLSearchParams();
42309
+ for (const [key, value] of Object.entries(params)) if (value !== void 0) search.set(key, value);
42310
+ const query = search.toString();
42311
+ return query ? `${url}?${query}` : url;
42312
+ }
42313
+ function trackUnmapped(input) {
42314
+ switch (input.entity) {
42315
+ case "calendar_day":
42316
+ case "report": return {
42317
+ url: null,
42318
+ note: "Track calendar and reports are scoped to a Track workspace; a Toggl 2.0 workspace ID is not a Track workspace ID."
42319
+ };
42320
+ default: return {
42321
+ url: null,
42322
+ note: `Toggl 2.0 ${input.entity.replace("_", " ")} IDs are not Track IDs (imports and Track sync create new records), so no Track URL maps.`
42323
+ };
42324
+ }
42325
+ }
42326
+ /** Builds the links for input that already passed `deepLinkInputError`. */
42327
+ function buildDeepLinks(input, context) {
42328
+ const origin = context.focusBaseUrl.replace(/\/+$/, "");
42329
+ const workspaceRoot = `${origin}/${context.organizationId}/workspaces/${context.workspaceId}`;
42330
+ const trackBase = trackBaseUrlFor(origin);
42331
+ let focus;
42332
+ let track = trackUnmapped(input);
42333
+ switch (input.entity) {
42334
+ case "task":
42335
+ focus = {
42336
+ url: withQuery(input.project_id !== void 0 ? `${workspaceRoot}/projects/${input.project_id}` : `${workspaceRoot}/calendar`, { tid: String(input.id) }),
42337
+ note: input.project_id !== void 0 ? "Opens the task editor over its project." : "Opens the task editor over the calendar; pass project_id to open it over the project instead."
42338
+ };
42339
+ break;
42340
+ case "project":
42341
+ focus = { url: `${workspaceRoot}/projects/${input.id}${input.tab ? `/${input.tab}` : ""}` };
42342
+ break;
42343
+ case "time_entry":
42344
+ if (input.task_id !== void 0) focus = {
42345
+ url: withQuery(`${workspaceRoot}/calendar`, {
42346
+ tid: String(input.task_id),
42347
+ teid: String(input.id)
42348
+ }),
42349
+ note: "Opens the time entry inside its task's editor."
42350
+ };
42351
+ else focus = {
42352
+ url: `${workspaceRoot}/calendar`,
42353
+ note: "A time entry is only addressable through its task; pass task_id to open it directly. Without it this is the calendar."
42354
+ };
42355
+ break;
42356
+ case "calendar_day":
42357
+ focus = {
42358
+ url: withQuery(`${workspaceRoot}/calendar`, { d: input.date }),
42359
+ note: input.date ? `Opens the week of ${input.date}.` : "Opens the current week."
42360
+ };
42361
+ break;
42362
+ case "report": {
42363
+ const reportType = input.report_type ?? "summary";
42364
+ const period = input.period !== void 0 ? { preset: input.period } : input.from !== void 0 ? {
42365
+ from: input.from,
42366
+ to: input.to
42367
+ } : void 0;
42368
+ focus = { url: withQuery(`${workspaceRoot}/reports/${reportType}`, { pe: period ? JSON.stringify(period) : void 0 }) };
42369
+ break;
42370
+ }
42371
+ case "member":
42372
+ focus = { url: `${workspaceRoot}/members/${input.id}${input.tab ? `/${input.tab}` : ""}` };
42373
+ track = trackBase ? {
42374
+ url: `${trackBase}/organization/${context.organizationId}/members/${input.id}/time-off`,
42375
+ note: input.tab === "time-off" ? void 0 : "Track has no member profile page; this is the member's Track time-off page."
42376
+ } : {
42377
+ url: null,
42378
+ note: `No Track host is known for ${new URL(origin).hostname}.`
42379
+ };
42380
+ break;
42381
+ }
42382
+ return {
42383
+ entity: input.entity,
42384
+ organization_id: context.organizationId,
42385
+ workspace_id: context.workspaceId,
42386
+ focus,
42387
+ track
42388
+ };
42389
+ }
42390
+ const deepLinkOperations = [{
42391
+ id: "focus.deep-links.get",
42392
+ family: "focus",
42393
+ resource: "deep-links",
42394
+ action: "get",
42395
+ title: "Get deep links",
42396
+ description: "Build Toggl 2.0 web URLs (and Track URLs where IDs map) for a task, project, time entry, calendar day, report or member. Read-only; makes no API calls.",
42397
+ agentGuidance: "member id is the organization user ID. time_entry needs task_id to open the entry itself. report_type defaults to summary; set period (preset) or from/to (YYYY-MM-DD) for a date range. track.url is null with a note when the IDs do not map to Track.",
42398
+ safety: "read",
42399
+ destructive: false,
42400
+ cli: {
42401
+ group: [],
42402
+ command: "deep-link",
42403
+ interactive: "hidden",
42404
+ examples: [{
42405
+ description: "Link to a task",
42406
+ data: {
42407
+ entity: "task",
42408
+ id: 123
42409
+ }
42410
+ }, {
42411
+ description: "Link to last month's detailed report",
42412
+ data: {
42413
+ entity: "report",
42414
+ report_type: "detailed",
42415
+ period: "prevMonth"
42416
+ }
42417
+ }]
42418
+ },
42419
+ mcp: { entityTool: "deep-links" },
42420
+ inputSchema: DeepLinkInputSchema,
42421
+ execute: (input, { client }) => {
42422
+ const deepLinkInput = input;
42423
+ const error = deepLinkInputError(deepLinkInput);
42424
+ if (error) return Promise.resolve({
42425
+ ok: false,
42426
+ status: 400,
42427
+ message: error
42428
+ });
42429
+ return client.runGeneratedRequest("getDeepLinks", () => Promise.resolve(buildDeepLinks(deepLinkInput, {
42430
+ focusBaseUrl: client.focusBaseUrl,
42431
+ organizationId: client.organizationId,
42432
+ workspaceId: client.workspaceIdNumber
42433
+ })));
42434
+ }
42435
+ }];
42212
42436
  const dateRangeDay = /* @__PURE__ */ pipe$2(/* @__PURE__ */ string$1(), /* @__PURE__ */ regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD"));
42213
42437
  const TimeOffMeUpdateInputSchema = /* @__PURE__ */ intersect([/* @__PURE__ */ object$2({ timeoff_id: /* @__PURE__ */ number$2() }), PublicMeEntryRequestSchema]);
42214
42438
  const emptySchema = /* @__PURE__ */ object$2({});
@@ -57889,7 +58113,11 @@ function applyOverrides(operation) {
57889
58113
  }
57890
58114
  };
57891
58115
  }
57892
- const operations = [...generatedFocusOperations.map((operation) => applyOverrides(operation)), ...orgSharedOperations];
58116
+ const operations = [
58117
+ ...generatedFocusOperations.map((operation) => applyOverrides(operation)),
58118
+ ...orgSharedOperations,
58119
+ ...deepLinkOperations
58120
+ ];
57893
58121
  function visibleOperations() {
57894
58122
  return operations.filter((operation) => !operation.hidden);
57895
58123
  }
@@ -59004,7 +59232,7 @@ function registerWorkspaceTools(server) {
59004
59232
  //#region src/package-version.ts
59005
59233
  function mcpPackageVersion() {
59006
59234
  try {
59007
- return "1.13.140";
59235
+ return "1.13.141";
59008
59236
  } catch {
59009
59237
  return "unknown";
59010
59238
  }
@@ -59185,7 +59413,7 @@ import_main.default.config({
59185
59413
  quiet: true,
59186
59414
  ignore: ["MISSING_ENV_FILE"]
59187
59415
  });
59188
- const packageVersion = "1.13.140";
59416
+ const packageVersion = "1.13.141";
59189
59417
  const MCP_CACHE_TTL_MS = 300 * 1e3;
59190
59418
  const MCP_MAX_TOOL_INPUT_ELEMENTS = 1e4;
59191
59419
  function resolvePublicUrls() {
@@ -59305,4 +59533,4 @@ function bootstrapServer(options = {}) {
59305
59533
  //#endregion
59306
59534
  export { PUBLIC_SENTRY_PRIVACY_OPTIONS as a, initProductAnalytics as c, shutdownProductAnalytics as d, runMcpCli as i, resolveProductAnalyticsDistinctId as l, MCP_MAX_TOOL_INPUT_ELEMENTS as n, captureToolInvocation as o, bootstrapServer as r, getProductAnalyticsDistinctId as s, MCP_CACHE_TTL_MS as t, resolvePublicSentryDsn as u };
59307
59535
 
59308
- //# sourceMappingURL=src-D5fshNIt.js.map
59536
+ //# sourceMappingURL=src-ZNZfSwsi.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@togglhq/mcp",
3
- "version": "1.13.140",
3
+ "version": "1.13.141",
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",