@yakirmar/mcp-toggl 1.1.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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/cache-manager.d.ts +43 -0
- package/dist/cache-manager.d.ts.map +1 -0
- package/dist/cache-manager.js +484 -0
- package/dist/cache-manager.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1264 -0
- package/dist/index.js.map +1 -0
- package/dist/timeline.d.ts +24 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +55 -0
- package/dist/timeline.js.map +1 -0
- package/dist/toggl-api.d.ts +55 -0
- package/dist/toggl-api.d.ts.map +1 -0
- package/dist/toggl-api.js +402 -0
- package/dist/toggl-api.js.map +1 -0
- package/dist/types.d.ts +268 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +48 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +439 -0
- package/dist/utils.js.map +1 -0
- package/dist/workspace.d.ts +22 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +65 -0
- package/dist/workspace.js.map +1 -0
- package/docs/images/drift-detection.svg +82 -0
- package/docs/images/monthly-heatmap.svg +124 -0
- package/docs/images/weekly-recap.svg +97 -0
- package/package.json +75 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { config } from 'dotenv';
|
|
6
|
+
import { TogglAPI, TimelineNotEnabledError, TogglAPIError } from './toggl-api.js';
|
|
7
|
+
import { buildTimelineResponse } from './timeline.js';
|
|
8
|
+
import { CacheManager } from './cache-manager.js';
|
|
9
|
+
import { WorkspaceResolutionError, parseWorkspaceId, resolveWorkspaceId, resolveProjectForClient, } from './workspace.js';
|
|
10
|
+
import { buildTimeEntryInterval, filterHydratedEntries, getDateRange, generateDailyReport, generateWeeklyReport, formatReportForDisplay, secondsToHours, groupEntriesByProject, groupEntriesByWorkspace, generateProjectSummary, generateWorkspaceSummary, toLocalYMD, parseLocalYMD, localDateRangeFromArgs, } from './utils.js';
|
|
11
|
+
function parseInclusiveEndDate(value) {
|
|
12
|
+
const date = parseLocalYMD(value);
|
|
13
|
+
date.setDate(date.getDate() + 1);
|
|
14
|
+
return date;
|
|
15
|
+
}
|
|
16
|
+
// Parse a required positive-integer entity id from tool arguments.
|
|
17
|
+
function requireEntryId(value) {
|
|
18
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
19
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
20
|
+
throw new Error('time_entry_id is required and must be a positive integer.');
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
function jsonResponse(data) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: 'text',
|
|
29
|
+
text: JSON.stringify(data, null, 2),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function errorMessage(error) {
|
|
35
|
+
return error instanceof Error ? error.message : 'An error occurred';
|
|
36
|
+
}
|
|
37
|
+
function errorPayload(error) {
|
|
38
|
+
const payload = {
|
|
39
|
+
error: true,
|
|
40
|
+
message: errorMessage(error),
|
|
41
|
+
};
|
|
42
|
+
if (error instanceof TogglAPIError) {
|
|
43
|
+
payload.code = error.code;
|
|
44
|
+
payload.status = error.status;
|
|
45
|
+
if (error.retry_after_seconds !== undefined) {
|
|
46
|
+
payload.retry_after_seconds = error.retry_after_seconds;
|
|
47
|
+
}
|
|
48
|
+
if (error.tip) {
|
|
49
|
+
payload.tip = error.tip;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (error instanceof WorkspaceResolutionError) {
|
|
53
|
+
payload.code = error.code;
|
|
54
|
+
payload.tip = error.tip;
|
|
55
|
+
payload.available_workspaces = error.available_workspaces;
|
|
56
|
+
}
|
|
57
|
+
return payload;
|
|
58
|
+
}
|
|
59
|
+
// Version for CLI output and server metadata
|
|
60
|
+
const VERSION = '1.1.0';
|
|
61
|
+
// Basic CLI flags: --help / -h and --version / -v
|
|
62
|
+
const argv = process.argv.slice(2);
|
|
63
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
64
|
+
console.error(`mcp-toggl version ${VERSION}`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
68
|
+
console.error(`mcp-toggl - Toggl MCP Server\n\n` +
|
|
69
|
+
`Usage:\n` +
|
|
70
|
+
` npx @verygoodplugins/mcp-toggl@latest [--help] [--version]\n\n` +
|
|
71
|
+
`Environment:\n` +
|
|
72
|
+
` TOGGL_API_KEY Required Toggl API token\n` +
|
|
73
|
+
` TOGGL_DEFAULT_WORKSPACE_ID Optional default workspace id\n` +
|
|
74
|
+
` TOGGL_CACHE_TTL Cache TTL in ms (default: 3600000)\n` +
|
|
75
|
+
` TOGGL_CACHE_SIZE Max cached entities (default: 1000)\n\n` +
|
|
76
|
+
`Claude Desktop (claude_desktop_config.json):\n` +
|
|
77
|
+
` {\n` +
|
|
78
|
+
` "mcpServers": {\n` +
|
|
79
|
+
` "mcp-toggl": {\n` +
|
|
80
|
+
` "command": "npx @verygoodplugins/mcp-toggl@latest",\n` +
|
|
81
|
+
` "env": { "TOGGL_API_KEY": "your_api_key_here" }\n` +
|
|
82
|
+
` }\n` +
|
|
83
|
+
` }\n` +
|
|
84
|
+
` }\n\n` +
|
|
85
|
+
`Cursor (~/.cursor/mcp.json):\n` +
|
|
86
|
+
` {\n` +
|
|
87
|
+
` "mcp": {\n` +
|
|
88
|
+
` "servers": {\n` +
|
|
89
|
+
` "mcp-toggl": {\n` +
|
|
90
|
+
` "command": "npx",\n` +
|
|
91
|
+
` "args": ["@verygoodplugins/mcp-toggl@latest"],\n` +
|
|
92
|
+
` "env": { "TOGGL_API_KEY": "your_api_key_here" }\n` +
|
|
93
|
+
` }\n` +
|
|
94
|
+
` }\n` +
|
|
95
|
+
` }\n` +
|
|
96
|
+
` }\n`);
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
// Load environment variables
|
|
100
|
+
config({ quiet: true });
|
|
101
|
+
// Validate required environment variables
|
|
102
|
+
// Support a few aliases for convenience/backward-compat
|
|
103
|
+
const RAW_API_KEY = process.env.TOGGL_API_KEY || process.env.TOGGL_API_TOKEN || process.env.TOGGL_TOKEN;
|
|
104
|
+
const API_KEY = RAW_API_KEY?.trim();
|
|
105
|
+
if (!API_KEY) {
|
|
106
|
+
console.error('Missing required environment variable: TOGGL_API_KEY');
|
|
107
|
+
console.error('Also accepted: TOGGL_API_TOKEN or TOGGL_TOKEN');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
if (process.env.TOGGL_API_TOKEN || process.env.TOGGL_TOKEN) {
|
|
111
|
+
console.warn('Using TOGGL_API_TOKEN/TOGGL_TOKEN. Prefer TOGGL_API_KEY going forward.');
|
|
112
|
+
}
|
|
113
|
+
// Initialize configuration
|
|
114
|
+
const cacheConfig = {
|
|
115
|
+
ttl: parseInt(process.env.TOGGL_CACHE_TTL || '3600000'),
|
|
116
|
+
maxSize: parseInt(process.env.TOGGL_CACHE_SIZE || '1000'),
|
|
117
|
+
batchSize: parseInt(process.env.TOGGL_BATCH_SIZE || '100'),
|
|
118
|
+
};
|
|
119
|
+
const defaultWorkspaceId = parseWorkspaceId(process.env.TOGGL_DEFAULT_WORKSPACE_ID);
|
|
120
|
+
// Initialize API and cache
|
|
121
|
+
const api = new TogglAPI(API_KEY);
|
|
122
|
+
const cache = new CacheManager(cacheConfig);
|
|
123
|
+
cache.setAPI(api);
|
|
124
|
+
// Track if cache has been warmed
|
|
125
|
+
let cacheWarmed = false;
|
|
126
|
+
// Helper to ensure cache is warm
|
|
127
|
+
async function ensureCache() {
|
|
128
|
+
if (!cacheWarmed) {
|
|
129
|
+
try {
|
|
130
|
+
const workspaces = await cache.getWorkspaces();
|
|
131
|
+
const singleWorkspaceId = workspaces.length === 1 ? workspaces[0].id : undefined;
|
|
132
|
+
const workspaceIdToWarm = defaultWorkspaceId || singleWorkspaceId;
|
|
133
|
+
if (workspaceIdToWarm) {
|
|
134
|
+
await cache.warmCache(workspaceIdToWarm);
|
|
135
|
+
}
|
|
136
|
+
cacheWarmed = true;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.error('Failed to warm cache:', error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function resolveWorkspaceForTool(args, action) {
|
|
144
|
+
return resolveWorkspaceId({
|
|
145
|
+
explicitWorkspaceId: args?.workspace_id,
|
|
146
|
+
defaultWorkspaceId,
|
|
147
|
+
getWorkspaces: () => cache.getWorkspaces(),
|
|
148
|
+
action,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Create MCP server
|
|
152
|
+
const server = new Server({
|
|
153
|
+
name: 'mcp-toggl',
|
|
154
|
+
version: VERSION,
|
|
155
|
+
}, {
|
|
156
|
+
capabilities: {
|
|
157
|
+
tools: {},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
// Define tool schemas
|
|
161
|
+
const tools = [
|
|
162
|
+
// Health/authentication
|
|
163
|
+
{
|
|
164
|
+
name: 'toggl_check_auth',
|
|
165
|
+
description: 'Verify Toggl API connectivity and authentication is valid',
|
|
166
|
+
annotations: {
|
|
167
|
+
readOnlyHint: true,
|
|
168
|
+
idempotentHint: true,
|
|
169
|
+
openWorldHint: true,
|
|
170
|
+
},
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {},
|
|
174
|
+
required: [],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
// Time tracking tools
|
|
178
|
+
{
|
|
179
|
+
name: 'toggl_get_time_entries',
|
|
180
|
+
description: 'Get time entries with optional date range filters. Returns hydrated entries with project/workspace names.',
|
|
181
|
+
annotations: {
|
|
182
|
+
readOnlyHint: true,
|
|
183
|
+
idempotentHint: true,
|
|
184
|
+
openWorldHint: true,
|
|
185
|
+
},
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
period: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
enum: ['today', 'yesterday', 'week', 'lastWeek', 'month', 'lastMonth'],
|
|
192
|
+
description: 'Predefined period to fetch entries for',
|
|
193
|
+
},
|
|
194
|
+
start_date: {
|
|
195
|
+
type: 'string',
|
|
196
|
+
description: 'Start date (YYYY-MM-DD format, inclusive, local timezone)',
|
|
197
|
+
},
|
|
198
|
+
end_date: {
|
|
199
|
+
type: 'string',
|
|
200
|
+
description: 'End date (YYYY-MM-DD format, inclusive, local timezone)',
|
|
201
|
+
},
|
|
202
|
+
workspace_id: {
|
|
203
|
+
type: 'number',
|
|
204
|
+
description: 'Filter by workspace ID',
|
|
205
|
+
},
|
|
206
|
+
project_id: {
|
|
207
|
+
type: 'number',
|
|
208
|
+
description: 'Filter by project ID',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'toggl_get_current_entry',
|
|
215
|
+
description: 'Get the currently running time entry, if any',
|
|
216
|
+
annotations: {
|
|
217
|
+
readOnlyHint: true,
|
|
218
|
+
idempotentHint: true,
|
|
219
|
+
openWorldHint: true,
|
|
220
|
+
},
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {},
|
|
224
|
+
required: [],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'toggl_start_timer',
|
|
229
|
+
description: 'Start a new time entry timer',
|
|
230
|
+
annotations: {
|
|
231
|
+
readOnlyHint: false,
|
|
232
|
+
idempotentHint: false,
|
|
233
|
+
openWorldHint: true,
|
|
234
|
+
},
|
|
235
|
+
inputSchema: {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: {
|
|
238
|
+
description: {
|
|
239
|
+
type: 'string',
|
|
240
|
+
description: 'Description of the time entry',
|
|
241
|
+
},
|
|
242
|
+
workspace_id: {
|
|
243
|
+
type: 'number',
|
|
244
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
245
|
+
},
|
|
246
|
+
project_id: {
|
|
247
|
+
type: 'number',
|
|
248
|
+
description: 'Project ID (optional)',
|
|
249
|
+
},
|
|
250
|
+
task_id: {
|
|
251
|
+
type: 'number',
|
|
252
|
+
description: 'Task ID (optional)',
|
|
253
|
+
},
|
|
254
|
+
tags: {
|
|
255
|
+
type: 'array',
|
|
256
|
+
items: { type: 'string' },
|
|
257
|
+
description: 'Tags for the entry',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'toggl_stop_timer',
|
|
264
|
+
description: 'Stop the currently running timer',
|
|
265
|
+
annotations: {
|
|
266
|
+
readOnlyHint: false,
|
|
267
|
+
idempotentHint: false,
|
|
268
|
+
openWorldHint: true,
|
|
269
|
+
},
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: {},
|
|
273
|
+
required: [],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'toggl_create_entry',
|
|
278
|
+
description: 'Create a completed (past) time entry for a project, optionally scoped to a client, with a start time and either an end time or a length in minutes. ' +
|
|
279
|
+
'Times are ISO 8601 datetimes: use Z or an offset (2026-07-11T09:00:00Z / 2026-07-11T09:00:00+03:00) for an absolute time, omit the offset for the host local timezone, or pass a bare YYYY-MM-DD for local midnight. ' +
|
|
280
|
+
'Provide exactly one of end or duration_minutes. Use toggl_start_timer instead to begin a currently-running timer.',
|
|
281
|
+
annotations: {
|
|
282
|
+
readOnlyHint: false,
|
|
283
|
+
idempotentHint: false,
|
|
284
|
+
openWorldHint: true,
|
|
285
|
+
},
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: 'object',
|
|
288
|
+
properties: {
|
|
289
|
+
description: {
|
|
290
|
+
type: 'string',
|
|
291
|
+
description: 'Description of the time entry',
|
|
292
|
+
},
|
|
293
|
+
workspace_id: {
|
|
294
|
+
type: 'number',
|
|
295
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
296
|
+
},
|
|
297
|
+
project_id: {
|
|
298
|
+
type: 'number',
|
|
299
|
+
description: 'Project to attach the entry to (optional).',
|
|
300
|
+
},
|
|
301
|
+
client_id: {
|
|
302
|
+
type: 'number',
|
|
303
|
+
description: 'Client to attach the entry to (optional). Toggl entries attach to a project, not a client directly: when set without project_id the client must have exactly one project; when both are set the project must belong to this client.',
|
|
304
|
+
},
|
|
305
|
+
task_id: {
|
|
306
|
+
type: 'number',
|
|
307
|
+
description: 'Task ID (optional).',
|
|
308
|
+
},
|
|
309
|
+
tags: {
|
|
310
|
+
type: 'array',
|
|
311
|
+
items: { type: 'string' },
|
|
312
|
+
description: 'Tags for the entry',
|
|
313
|
+
},
|
|
314
|
+
billable: {
|
|
315
|
+
type: 'boolean',
|
|
316
|
+
description: 'Whether the entry is billable (optional).',
|
|
317
|
+
},
|
|
318
|
+
start: {
|
|
319
|
+
type: 'string',
|
|
320
|
+
description: 'Start time as an ISO 8601 datetime (required).',
|
|
321
|
+
},
|
|
322
|
+
end: {
|
|
323
|
+
type: 'string',
|
|
324
|
+
description: 'End time as an ISO 8601 datetime. Provide this or duration_minutes.',
|
|
325
|
+
},
|
|
326
|
+
duration_minutes: {
|
|
327
|
+
type: 'number',
|
|
328
|
+
description: 'Length of the entry in minutes. Provide this or end.',
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
required: ['start'],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'toggl_search_entries',
|
|
336
|
+
description: 'Search time entries and return matches including their id (use the id with toggl_delete_entry). ' +
|
|
337
|
+
'All filters combine with AND; text filters (description, project_name, client_name, tag) are case-insensitive substring matches. ' +
|
|
338
|
+
'Date window: pass period or start_date/end_date (YYYY-MM-DD, inclusive, local); defaults to roughly the last 31 days. ' +
|
|
339
|
+
'start_after/start_before further narrow by entry start time (ISO 8601 datetime). ' +
|
|
340
|
+
'Results are hydrated with project/workspace/client/tag names and sorted newest-first.',
|
|
341
|
+
annotations: {
|
|
342
|
+
readOnlyHint: true,
|
|
343
|
+
idempotentHint: true,
|
|
344
|
+
openWorldHint: true,
|
|
345
|
+
},
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: 'object',
|
|
348
|
+
properties: {
|
|
349
|
+
description: {
|
|
350
|
+
type: 'string',
|
|
351
|
+
description: 'Case-insensitive substring match on the entry description.',
|
|
352
|
+
},
|
|
353
|
+
project_name: {
|
|
354
|
+
type: 'string',
|
|
355
|
+
description: 'Case-insensitive substring match on the project name.',
|
|
356
|
+
},
|
|
357
|
+
client_name: {
|
|
358
|
+
type: 'string',
|
|
359
|
+
description: 'Case-insensitive substring match on the client name.',
|
|
360
|
+
},
|
|
361
|
+
project_id: {
|
|
362
|
+
type: 'number',
|
|
363
|
+
description: 'Exact project id to match.',
|
|
364
|
+
},
|
|
365
|
+
client_id: {
|
|
366
|
+
type: 'number',
|
|
367
|
+
description: 'Exact client id to match.',
|
|
368
|
+
},
|
|
369
|
+
workspace_id: {
|
|
370
|
+
type: 'number',
|
|
371
|
+
description: 'Exact workspace id to match.',
|
|
372
|
+
},
|
|
373
|
+
tag: {
|
|
374
|
+
type: 'string',
|
|
375
|
+
description: 'Case-insensitive substring match against any of the entry tags.',
|
|
376
|
+
},
|
|
377
|
+
billable: {
|
|
378
|
+
type: 'boolean',
|
|
379
|
+
description: 'Match only billable (true) or non-billable (false) entries.',
|
|
380
|
+
},
|
|
381
|
+
period: {
|
|
382
|
+
type: 'string',
|
|
383
|
+
enum: ['today', 'yesterday', 'week', 'lastWeek', 'month', 'lastMonth'],
|
|
384
|
+
description: 'Predefined date window to search within (alternative to start_date/end_date).',
|
|
385
|
+
},
|
|
386
|
+
start_date: {
|
|
387
|
+
type: 'string',
|
|
388
|
+
description: 'Window start (YYYY-MM-DD, inclusive, local timezone).',
|
|
389
|
+
},
|
|
390
|
+
end_date: {
|
|
391
|
+
type: 'string',
|
|
392
|
+
description: 'Window end (YYYY-MM-DD, inclusive, local timezone).',
|
|
393
|
+
},
|
|
394
|
+
start_after: {
|
|
395
|
+
type: 'string',
|
|
396
|
+
description: 'Only entries starting at or after this ISO 8601 datetime.',
|
|
397
|
+
},
|
|
398
|
+
start_before: {
|
|
399
|
+
type: 'string',
|
|
400
|
+
description: 'Only entries starting at or before this ISO 8601 datetime.',
|
|
401
|
+
},
|
|
402
|
+
min_duration_minutes: {
|
|
403
|
+
type: 'number',
|
|
404
|
+
description: 'Only entries at least this many minutes long.',
|
|
405
|
+
},
|
|
406
|
+
max_duration_minutes: {
|
|
407
|
+
type: 'number',
|
|
408
|
+
description: 'Only entries at most this many minutes long.',
|
|
409
|
+
},
|
|
410
|
+
limit: {
|
|
411
|
+
type: 'number',
|
|
412
|
+
minimum: 1,
|
|
413
|
+
maximum: 1000,
|
|
414
|
+
default: 50,
|
|
415
|
+
description: 'Maximum number of matches to return (default: 50, max: 1000).',
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: 'toggl_delete_entry',
|
|
422
|
+
description: 'Delete a time entry by its id. If workspace_id is omitted it is resolved from the entry itself. ' +
|
|
423
|
+
'Use toggl_search_entries first to find the id when you do not already have it. This cannot be undone.',
|
|
424
|
+
annotations: {
|
|
425
|
+
readOnlyHint: false,
|
|
426
|
+
idempotentHint: false,
|
|
427
|
+
destructiveHint: true,
|
|
428
|
+
openWorldHint: true,
|
|
429
|
+
},
|
|
430
|
+
inputSchema: {
|
|
431
|
+
type: 'object',
|
|
432
|
+
properties: {
|
|
433
|
+
time_entry_id: {
|
|
434
|
+
type: 'number',
|
|
435
|
+
description: 'Id of the time entry to delete (required).',
|
|
436
|
+
},
|
|
437
|
+
workspace_id: {
|
|
438
|
+
type: 'number',
|
|
439
|
+
description: 'Workspace the entry belongs to. If omitted, it is looked up from the entry.',
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
required: ['time_entry_id'],
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
// Reporting tools
|
|
446
|
+
{
|
|
447
|
+
name: 'toggl_daily_report',
|
|
448
|
+
description: 'Generate a daily report with hours by project and workspace',
|
|
449
|
+
annotations: {
|
|
450
|
+
readOnlyHint: true,
|
|
451
|
+
idempotentHint: true,
|
|
452
|
+
openWorldHint: true,
|
|
453
|
+
},
|
|
454
|
+
inputSchema: {
|
|
455
|
+
type: 'object',
|
|
456
|
+
properties: {
|
|
457
|
+
date: {
|
|
458
|
+
type: 'string',
|
|
459
|
+
description: 'Date for report (YYYY-MM-DD format, defaults to today)',
|
|
460
|
+
},
|
|
461
|
+
format: {
|
|
462
|
+
type: 'string',
|
|
463
|
+
enum: ['json', 'text'],
|
|
464
|
+
description: 'Output format (default: json)',
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: 'toggl_weekly_report',
|
|
471
|
+
description: 'Generate a weekly report with daily breakdown and project summaries',
|
|
472
|
+
annotations: {
|
|
473
|
+
readOnlyHint: true,
|
|
474
|
+
idempotentHint: true,
|
|
475
|
+
openWorldHint: true,
|
|
476
|
+
},
|
|
477
|
+
inputSchema: {
|
|
478
|
+
type: 'object',
|
|
479
|
+
properties: {
|
|
480
|
+
week_offset: {
|
|
481
|
+
type: 'number',
|
|
482
|
+
description: 'Week offset from current week (0 = this week, -1 = last week)',
|
|
483
|
+
},
|
|
484
|
+
format: {
|
|
485
|
+
type: 'string',
|
|
486
|
+
enum: ['json', 'text'],
|
|
487
|
+
description: 'Output format (default: json)',
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: 'toggl_project_summary',
|
|
494
|
+
description: 'Get total hours per project for a date range',
|
|
495
|
+
annotations: {
|
|
496
|
+
readOnlyHint: true,
|
|
497
|
+
idempotentHint: true,
|
|
498
|
+
openWorldHint: true,
|
|
499
|
+
},
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: 'object',
|
|
502
|
+
properties: {
|
|
503
|
+
period: {
|
|
504
|
+
type: 'string',
|
|
505
|
+
enum: ['week', 'lastWeek', 'month', 'lastMonth'],
|
|
506
|
+
description: 'Predefined period',
|
|
507
|
+
},
|
|
508
|
+
start_date: {
|
|
509
|
+
type: 'string',
|
|
510
|
+
description: 'Start date (YYYY-MM-DD format, inclusive, local timezone)',
|
|
511
|
+
},
|
|
512
|
+
end_date: {
|
|
513
|
+
type: 'string',
|
|
514
|
+
description: 'End date (YYYY-MM-DD format, inclusive, local timezone)',
|
|
515
|
+
},
|
|
516
|
+
workspace_id: {
|
|
517
|
+
type: 'number',
|
|
518
|
+
description: 'Filter by workspace ID',
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
name: 'toggl_workspace_summary',
|
|
525
|
+
description: 'Get total hours per workspace for a date range',
|
|
526
|
+
annotations: {
|
|
527
|
+
readOnlyHint: true,
|
|
528
|
+
idempotentHint: true,
|
|
529
|
+
openWorldHint: true,
|
|
530
|
+
},
|
|
531
|
+
inputSchema: {
|
|
532
|
+
type: 'object',
|
|
533
|
+
properties: {
|
|
534
|
+
period: {
|
|
535
|
+
type: 'string',
|
|
536
|
+
enum: ['week', 'lastWeek', 'month', 'lastMonth'],
|
|
537
|
+
description: 'Predefined period',
|
|
538
|
+
},
|
|
539
|
+
start_date: {
|
|
540
|
+
type: 'string',
|
|
541
|
+
description: 'Start date (YYYY-MM-DD format, inclusive, local timezone)',
|
|
542
|
+
},
|
|
543
|
+
end_date: {
|
|
544
|
+
type: 'string',
|
|
545
|
+
description: 'End date (YYYY-MM-DD format, inclusive, local timezone)',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
// Management tools
|
|
551
|
+
{
|
|
552
|
+
name: 'toggl_list_workspaces',
|
|
553
|
+
description: 'List all available workspaces',
|
|
554
|
+
annotations: {
|
|
555
|
+
readOnlyHint: true,
|
|
556
|
+
idempotentHint: true,
|
|
557
|
+
openWorldHint: true,
|
|
558
|
+
},
|
|
559
|
+
inputSchema: {
|
|
560
|
+
type: 'object',
|
|
561
|
+
properties: {},
|
|
562
|
+
required: [],
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: 'toggl_list_projects',
|
|
567
|
+
description: 'List projects for a workspace',
|
|
568
|
+
annotations: {
|
|
569
|
+
readOnlyHint: true,
|
|
570
|
+
idempotentHint: true,
|
|
571
|
+
openWorldHint: true,
|
|
572
|
+
},
|
|
573
|
+
inputSchema: {
|
|
574
|
+
type: 'object',
|
|
575
|
+
properties: {
|
|
576
|
+
workspace_id: {
|
|
577
|
+
type: 'number',
|
|
578
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: 'toggl_list_clients',
|
|
585
|
+
description: 'List clients for a workspace',
|
|
586
|
+
annotations: {
|
|
587
|
+
readOnlyHint: true,
|
|
588
|
+
idempotentHint: true,
|
|
589
|
+
openWorldHint: true,
|
|
590
|
+
},
|
|
591
|
+
inputSchema: {
|
|
592
|
+
type: 'object',
|
|
593
|
+
properties: {
|
|
594
|
+
workspace_id: {
|
|
595
|
+
type: 'number',
|
|
596
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
// Cache management
|
|
602
|
+
{
|
|
603
|
+
name: 'toggl_warm_cache',
|
|
604
|
+
description: 'Pre-fetch and cache workspace, project, client, and tag data for better performance',
|
|
605
|
+
annotations: {
|
|
606
|
+
readOnlyHint: true,
|
|
607
|
+
idempotentHint: true,
|
|
608
|
+
openWorldHint: true,
|
|
609
|
+
},
|
|
610
|
+
inputSchema: {
|
|
611
|
+
type: 'object',
|
|
612
|
+
properties: {
|
|
613
|
+
workspace_id: {
|
|
614
|
+
type: 'number',
|
|
615
|
+
description: 'Workspace ID to warm. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
name: 'toggl_cache_stats',
|
|
622
|
+
description: 'Get cache statistics and performance metrics',
|
|
623
|
+
annotations: {
|
|
624
|
+
readOnlyHint: true,
|
|
625
|
+
idempotentHint: true,
|
|
626
|
+
openWorldHint: false,
|
|
627
|
+
},
|
|
628
|
+
inputSchema: {
|
|
629
|
+
type: 'object',
|
|
630
|
+
properties: {},
|
|
631
|
+
required: [],
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: 'toggl_clear_cache',
|
|
636
|
+
description: 'Clear all cached data',
|
|
637
|
+
annotations: {
|
|
638
|
+
readOnlyHint: false,
|
|
639
|
+
idempotentHint: true,
|
|
640
|
+
openWorldHint: false,
|
|
641
|
+
},
|
|
642
|
+
inputSchema: {
|
|
643
|
+
type: 'object',
|
|
644
|
+
properties: {},
|
|
645
|
+
required: [],
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: 'toggl_get_timeline',
|
|
650
|
+
description: 'Get Toggl Desktop activity timeline showing application usage. PRIVACY NOTE: raw events include window titles that may contain sensitive document names, email subjects, chat text, URLs, OAuth pages, or database names; use include_events: false for privacy-conscious summary-only usage. Requires Toggl Track Desktop timeline sync to be enabled. Response semantics: summary is { [appName: string]: total_seconds }; total_events is the post-filter event count; returned_events is the returned events array length; truncated means only the events array was limited, never the summary. limit does not affect summary calculation. total_seconds is canonical; total_hours is rounded to 4 decimals for display.',
|
|
651
|
+
annotations: {
|
|
652
|
+
readOnlyHint: true,
|
|
653
|
+
idempotentHint: true,
|
|
654
|
+
openWorldHint: true,
|
|
655
|
+
},
|
|
656
|
+
inputSchema: {
|
|
657
|
+
type: 'object',
|
|
658
|
+
properties: {
|
|
659
|
+
period: {
|
|
660
|
+
type: 'string',
|
|
661
|
+
enum: ['today', 'yesterday', 'week', 'lastWeek', 'month', 'lastMonth'],
|
|
662
|
+
description: 'Predefined period (alternative to start_date/end_date)',
|
|
663
|
+
},
|
|
664
|
+
start_date: {
|
|
665
|
+
type: 'string',
|
|
666
|
+
description: 'Start date (YYYY-MM-DD format, inclusive, local timezone)',
|
|
667
|
+
},
|
|
668
|
+
end_date: {
|
|
669
|
+
type: 'string',
|
|
670
|
+
description: 'End date (YYYY-MM-DD format, inclusive, local timezone)',
|
|
671
|
+
},
|
|
672
|
+
app: {
|
|
673
|
+
type: 'string',
|
|
674
|
+
description: 'Filter by application name, case-insensitive partial match',
|
|
675
|
+
},
|
|
676
|
+
include_events: {
|
|
677
|
+
type: 'boolean',
|
|
678
|
+
description: 'Include raw events array (default: true). Set false for summary only.',
|
|
679
|
+
},
|
|
680
|
+
redact_titles: {
|
|
681
|
+
type: 'boolean',
|
|
682
|
+
default: false,
|
|
683
|
+
description: 'When true, returned events keep app name, timestamps, duration, idle state, and desktop_id, but set title to null.',
|
|
684
|
+
},
|
|
685
|
+
limit: {
|
|
686
|
+
type: 'number',
|
|
687
|
+
minimum: 1,
|
|
688
|
+
maximum: 1000,
|
|
689
|
+
default: 50,
|
|
690
|
+
description: 'Maximum events to return in events array (default: 50, max: 1000). Does not affect summary calculation.',
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
];
|
|
696
|
+
// Handle tool listing
|
|
697
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
698
|
+
return { tools };
|
|
699
|
+
});
|
|
700
|
+
// Handle tool calls
|
|
701
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
702
|
+
const { name, arguments: args } = request.params;
|
|
703
|
+
try {
|
|
704
|
+
switch (name) {
|
|
705
|
+
// Health/authentication
|
|
706
|
+
case 'toggl_check_auth': {
|
|
707
|
+
const me = await api.getMe();
|
|
708
|
+
const workspaces = await cache.getWorkspaces();
|
|
709
|
+
const maskEmail = (e) => {
|
|
710
|
+
if (!e)
|
|
711
|
+
return undefined;
|
|
712
|
+
const [user, domain] = e.split('@');
|
|
713
|
+
if (!domain)
|
|
714
|
+
return '***';
|
|
715
|
+
const u = user.length <= 2 ? '*'.repeat(user.length) : `${user[0]}***${user.slice(-1)}`;
|
|
716
|
+
return `${u}@${domain}`;
|
|
717
|
+
};
|
|
718
|
+
return {
|
|
719
|
+
content: [
|
|
720
|
+
{
|
|
721
|
+
type: 'text',
|
|
722
|
+
text: JSON.stringify({
|
|
723
|
+
authenticated: true,
|
|
724
|
+
user: {
|
|
725
|
+
id: me.id,
|
|
726
|
+
email: maskEmail(me.email),
|
|
727
|
+
fullname: me.fullname,
|
|
728
|
+
},
|
|
729
|
+
workspaces: workspaces.map((w) => ({ id: w.id, name: w.name })),
|
|
730
|
+
}, null, 2),
|
|
731
|
+
},
|
|
732
|
+
],
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// Time tracking tools
|
|
736
|
+
case 'toggl_get_time_entries': {
|
|
737
|
+
await ensureCache();
|
|
738
|
+
let entries;
|
|
739
|
+
if (args?.period) {
|
|
740
|
+
const range = getDateRange(args.period);
|
|
741
|
+
entries = await api.getTimeEntriesForDateRange(range.start, range.end);
|
|
742
|
+
}
|
|
743
|
+
else if (args?.start_date || args?.end_date) {
|
|
744
|
+
const start = args?.start_date ? parseLocalYMD(args.start_date) : new Date();
|
|
745
|
+
start.setHours(0, 0, 0, 0);
|
|
746
|
+
const end = args?.end_date ? parseInclusiveEndDate(args.end_date) : new Date();
|
|
747
|
+
if (!args?.end_date) {
|
|
748
|
+
end.setHours(0, 0, 0, 0);
|
|
749
|
+
end.setDate(end.getDate() + 1);
|
|
750
|
+
}
|
|
751
|
+
entries = await api.getTimeEntriesForDateRange(start, end);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
entries = await api.getTimeEntriesForToday();
|
|
755
|
+
}
|
|
756
|
+
// Filter by workspace/project if specified
|
|
757
|
+
if (args?.workspace_id) {
|
|
758
|
+
entries = entries.filter((e) => e.workspace_id === args.workspace_id);
|
|
759
|
+
}
|
|
760
|
+
if (args?.project_id) {
|
|
761
|
+
entries = entries.filter((e) => e.project_id === args.project_id);
|
|
762
|
+
}
|
|
763
|
+
// Hydrate with names
|
|
764
|
+
const hydrated = await cache.hydrateTimeEntries(entries);
|
|
765
|
+
return {
|
|
766
|
+
content: [
|
|
767
|
+
{
|
|
768
|
+
type: 'text',
|
|
769
|
+
text: JSON.stringify({
|
|
770
|
+
count: hydrated.length,
|
|
771
|
+
entries: hydrated,
|
|
772
|
+
}, null, 2),
|
|
773
|
+
},
|
|
774
|
+
],
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
case 'toggl_get_current_entry': {
|
|
778
|
+
const entry = await api.getCurrentTimeEntry();
|
|
779
|
+
if (!entry) {
|
|
780
|
+
return {
|
|
781
|
+
content: [
|
|
782
|
+
{
|
|
783
|
+
type: 'text',
|
|
784
|
+
text: JSON.stringify({
|
|
785
|
+
running: false,
|
|
786
|
+
message: 'No timer currently running',
|
|
787
|
+
}),
|
|
788
|
+
},
|
|
789
|
+
],
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
await ensureCache();
|
|
793
|
+
const hydrated = await cache.hydrateTimeEntries([entry]);
|
|
794
|
+
return {
|
|
795
|
+
content: [
|
|
796
|
+
{
|
|
797
|
+
type: 'text',
|
|
798
|
+
text: JSON.stringify({
|
|
799
|
+
running: true,
|
|
800
|
+
entry: hydrated[0],
|
|
801
|
+
}, null, 2),
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
case 'toggl_start_timer': {
|
|
807
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'starting a timer');
|
|
808
|
+
const entry = await api.startTimer(workspaceId, args?.description, args?.project_id, args?.task_id, args?.tags);
|
|
809
|
+
await ensureCache();
|
|
810
|
+
const hydrated = await cache.hydrateTimeEntries([entry]);
|
|
811
|
+
return {
|
|
812
|
+
content: [
|
|
813
|
+
{
|
|
814
|
+
type: 'text',
|
|
815
|
+
text: JSON.stringify({
|
|
816
|
+
success: true,
|
|
817
|
+
message: 'Timer started',
|
|
818
|
+
entry: hydrated[0],
|
|
819
|
+
}, null, 2),
|
|
820
|
+
},
|
|
821
|
+
],
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
case 'toggl_stop_timer': {
|
|
825
|
+
const current = await api.getCurrentTimeEntry();
|
|
826
|
+
if (!current) {
|
|
827
|
+
return {
|
|
828
|
+
content: [
|
|
829
|
+
{
|
|
830
|
+
type: 'text',
|
|
831
|
+
text: JSON.stringify({
|
|
832
|
+
success: false,
|
|
833
|
+
message: 'No timer currently running',
|
|
834
|
+
}),
|
|
835
|
+
},
|
|
836
|
+
],
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const stopped = await api.stopTimer(current.workspace_id, current.id);
|
|
840
|
+
await ensureCache();
|
|
841
|
+
const hydrated = await cache.hydrateTimeEntries([stopped]);
|
|
842
|
+
return {
|
|
843
|
+
content: [
|
|
844
|
+
{
|
|
845
|
+
type: 'text',
|
|
846
|
+
text: JSON.stringify({
|
|
847
|
+
success: true,
|
|
848
|
+
message: 'Timer stopped',
|
|
849
|
+
entry: hydrated[0],
|
|
850
|
+
}, null, 2),
|
|
851
|
+
},
|
|
852
|
+
],
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
case 'toggl_create_entry': {
|
|
856
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'creating a time entry');
|
|
857
|
+
let projectId = args?.project_id;
|
|
858
|
+
const clientId = args?.client_id;
|
|
859
|
+
if (clientId !== undefined) {
|
|
860
|
+
const projects = await cache.getProjects(workspaceId);
|
|
861
|
+
projectId = resolveProjectForClient(projects, clientId, projectId);
|
|
862
|
+
}
|
|
863
|
+
const { start, stop, duration } = buildTimeEntryInterval({
|
|
864
|
+
start: args?.start,
|
|
865
|
+
end: args?.end,
|
|
866
|
+
duration_minutes: args?.duration_minutes,
|
|
867
|
+
});
|
|
868
|
+
const created = await api.createTimeEntry(workspaceId, {
|
|
869
|
+
description: args?.description,
|
|
870
|
+
project_id: projectId,
|
|
871
|
+
task_id: args?.task_id,
|
|
872
|
+
tags: args?.tags,
|
|
873
|
+
billable: args?.billable,
|
|
874
|
+
start,
|
|
875
|
+
stop,
|
|
876
|
+
duration,
|
|
877
|
+
});
|
|
878
|
+
await ensureCache();
|
|
879
|
+
const hydrated = await cache.hydrateTimeEntries([created]);
|
|
880
|
+
return {
|
|
881
|
+
content: [
|
|
882
|
+
{
|
|
883
|
+
type: 'text',
|
|
884
|
+
text: JSON.stringify({
|
|
885
|
+
success: true,
|
|
886
|
+
message: 'Time entry created',
|
|
887
|
+
entry: hydrated[0],
|
|
888
|
+
}, null, 2),
|
|
889
|
+
},
|
|
890
|
+
],
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
case 'toggl_search_entries': {
|
|
894
|
+
await ensureCache();
|
|
895
|
+
// Default to roughly the last 31 days; override with period or explicit dates.
|
|
896
|
+
const todayMidnight = new Date();
|
|
897
|
+
todayMidnight.setHours(0, 0, 0, 0);
|
|
898
|
+
let start = new Date(todayMidnight);
|
|
899
|
+
start.setDate(start.getDate() - 31);
|
|
900
|
+
let end = new Date(todayMidnight);
|
|
901
|
+
end.setDate(end.getDate() + 1);
|
|
902
|
+
if (args?.period) {
|
|
903
|
+
const range = getDateRange(args.period);
|
|
904
|
+
start = range.start;
|
|
905
|
+
end = range.end;
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
if (args?.start_date)
|
|
909
|
+
start = parseLocalYMD(args.start_date);
|
|
910
|
+
if (args?.end_date)
|
|
911
|
+
end = parseInclusiveEndDate(args.end_date);
|
|
912
|
+
}
|
|
913
|
+
const entries = await api.getTimeEntriesForDateRange(start, end);
|
|
914
|
+
const hydrated = await cache.hydrateTimeEntries(entries);
|
|
915
|
+
const matches = filterHydratedEntries(hydrated, {
|
|
916
|
+
description: args?.description,
|
|
917
|
+
project_name: args?.project_name,
|
|
918
|
+
client_name: args?.client_name,
|
|
919
|
+
project_id: args?.project_id,
|
|
920
|
+
client_id: args?.client_id,
|
|
921
|
+
workspace_id: args?.workspace_id,
|
|
922
|
+
tag: args?.tag,
|
|
923
|
+
billable: args?.billable,
|
|
924
|
+
start_after: args?.start_after,
|
|
925
|
+
start_before: args?.start_before,
|
|
926
|
+
min_duration_minutes: args?.min_duration_minutes,
|
|
927
|
+
max_duration_minutes: args?.max_duration_minutes,
|
|
928
|
+
});
|
|
929
|
+
// Newest first so the most likely deletion target is at the top.
|
|
930
|
+
matches.sort((a, b) => new Date(b.start).getTime() - new Date(a.start).getTime());
|
|
931
|
+
const requestedLimit = typeof args?.limit === 'number' ? args.limit : 50;
|
|
932
|
+
const limit = Math.min(Math.max(1, Math.floor(requestedLimit)), 1000);
|
|
933
|
+
const limited = matches.slice(0, limit);
|
|
934
|
+
return jsonResponse({
|
|
935
|
+
count: matches.length,
|
|
936
|
+
returned: limited.length,
|
|
937
|
+
truncated: matches.length > limited.length,
|
|
938
|
+
entries: limited,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
case 'toggl_delete_entry': {
|
|
942
|
+
const timeEntryId = requireEntryId(args?.time_entry_id);
|
|
943
|
+
let workspaceId = parseWorkspaceId(args?.workspace_id);
|
|
944
|
+
if (workspaceId === undefined) {
|
|
945
|
+
let existing;
|
|
946
|
+
try {
|
|
947
|
+
existing = await api.getTimeEntry(timeEntryId);
|
|
948
|
+
}
|
|
949
|
+
catch (error) {
|
|
950
|
+
throw new Error(`Time entry ${timeEntryId} was not found. Provide workspace_id, ` +
|
|
951
|
+
`or use toggl_search_entries to find a valid id.`, { cause: error });
|
|
952
|
+
}
|
|
953
|
+
if (!existing?.workspace_id) {
|
|
954
|
+
throw new Error(`Time entry ${timeEntryId} was not found. Use toggl_search_entries to find a valid id.`);
|
|
955
|
+
}
|
|
956
|
+
workspaceId = existing.workspace_id;
|
|
957
|
+
}
|
|
958
|
+
await api.deleteTimeEntry(workspaceId, timeEntryId);
|
|
959
|
+
return jsonResponse({
|
|
960
|
+
success: true,
|
|
961
|
+
message: 'Time entry deleted',
|
|
962
|
+
time_entry_id: timeEntryId,
|
|
963
|
+
workspace_id: workspaceId,
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
// Reporting tools
|
|
967
|
+
case 'toggl_daily_report': {
|
|
968
|
+
await ensureCache();
|
|
969
|
+
const date = args?.date ? parseLocalYMD(args.date) : new Date();
|
|
970
|
+
date.setHours(0, 0, 0, 0);
|
|
971
|
+
const nextDay = new Date(date);
|
|
972
|
+
nextDay.setDate(nextDay.getDate() + 1);
|
|
973
|
+
const entries = await api.getTimeEntriesForDateRange(date, nextDay);
|
|
974
|
+
const hydrated = await cache.hydrateTimeEntries(entries);
|
|
975
|
+
const report = generateDailyReport(toLocalYMD(date), hydrated);
|
|
976
|
+
if (args?.format === 'text') {
|
|
977
|
+
return {
|
|
978
|
+
content: [
|
|
979
|
+
{
|
|
980
|
+
type: 'text',
|
|
981
|
+
text: formatReportForDisplay(report),
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
content: [
|
|
988
|
+
{
|
|
989
|
+
type: 'text',
|
|
990
|
+
text: JSON.stringify(report, null, 2),
|
|
991
|
+
},
|
|
992
|
+
],
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
case 'toggl_weekly_report': {
|
|
996
|
+
await ensureCache();
|
|
997
|
+
const weekOffset = args?.week_offset || 0;
|
|
998
|
+
const entries = await api.getTimeEntriesForWeek(weekOffset);
|
|
999
|
+
const hydrated = await cache.hydrateTimeEntries(entries);
|
|
1000
|
+
// Calculate week boundaries in local time.
|
|
1001
|
+
const today = new Date();
|
|
1002
|
+
today.setHours(0, 0, 0, 0);
|
|
1003
|
+
const dayOfWeek = today.getDay();
|
|
1004
|
+
const diff = today.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
|
|
1005
|
+
const monday = new Date(today);
|
|
1006
|
+
monday.setDate(diff + weekOffset * 7);
|
|
1007
|
+
const sunday = new Date(monday);
|
|
1008
|
+
sunday.setDate(sunday.getDate() + 6);
|
|
1009
|
+
const report = generateWeeklyReport(monday, sunday, hydrated);
|
|
1010
|
+
if (args?.format === 'text') {
|
|
1011
|
+
return {
|
|
1012
|
+
content: [
|
|
1013
|
+
{
|
|
1014
|
+
type: 'text',
|
|
1015
|
+
text: formatReportForDisplay(report),
|
|
1016
|
+
},
|
|
1017
|
+
],
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
content: [
|
|
1022
|
+
{
|
|
1023
|
+
type: 'text',
|
|
1024
|
+
text: JSON.stringify(report, null, 2),
|
|
1025
|
+
},
|
|
1026
|
+
],
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
case 'toggl_project_summary': {
|
|
1030
|
+
await ensureCache();
|
|
1031
|
+
let entries;
|
|
1032
|
+
if (args?.period) {
|
|
1033
|
+
const range = getDateRange(args.period);
|
|
1034
|
+
entries = await api.getTimeEntriesForDateRange(range.start, range.end);
|
|
1035
|
+
}
|
|
1036
|
+
else if (args?.start_date && args?.end_date) {
|
|
1037
|
+
const start = parseLocalYMD(args.start_date);
|
|
1038
|
+
const end = parseInclusiveEndDate(args.end_date);
|
|
1039
|
+
entries = await api.getTimeEntriesForDateRange(start, end);
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
// Default to current week
|
|
1043
|
+
entries = await api.getTimeEntriesForWeek(0);
|
|
1044
|
+
}
|
|
1045
|
+
if (args?.workspace_id) {
|
|
1046
|
+
entries = entries.filter((e) => e.workspace_id === args.workspace_id);
|
|
1047
|
+
}
|
|
1048
|
+
const hydrated = await cache.hydrateTimeEntries(entries);
|
|
1049
|
+
const byProject = groupEntriesByProject(hydrated);
|
|
1050
|
+
const summaries = [];
|
|
1051
|
+
byProject.forEach((projectEntries, projectName) => {
|
|
1052
|
+
summaries.push(generateProjectSummary(projectName, projectEntries));
|
|
1053
|
+
});
|
|
1054
|
+
// Sort by total hours descending
|
|
1055
|
+
summaries.sort((a, b) => b.total_seconds - a.total_seconds);
|
|
1056
|
+
return {
|
|
1057
|
+
content: [
|
|
1058
|
+
{
|
|
1059
|
+
type: 'text',
|
|
1060
|
+
text: JSON.stringify({
|
|
1061
|
+
project_count: summaries.length,
|
|
1062
|
+
total_hours: secondsToHours(summaries.reduce((t, s) => t + s.total_seconds, 0)),
|
|
1063
|
+
projects: summaries,
|
|
1064
|
+
}, null, 2),
|
|
1065
|
+
},
|
|
1066
|
+
],
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
case 'toggl_workspace_summary': {
|
|
1070
|
+
await ensureCache();
|
|
1071
|
+
let entries;
|
|
1072
|
+
if (args?.period) {
|
|
1073
|
+
const range = getDateRange(args.period);
|
|
1074
|
+
entries = await api.getTimeEntriesForDateRange(range.start, range.end);
|
|
1075
|
+
}
|
|
1076
|
+
else if (args?.start_date && args?.end_date) {
|
|
1077
|
+
const start = parseLocalYMD(args.start_date);
|
|
1078
|
+
const end = parseInclusiveEndDate(args.end_date);
|
|
1079
|
+
entries = await api.getTimeEntriesForDateRange(start, end);
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
// Default to current week
|
|
1083
|
+
entries = await api.getTimeEntriesForWeek(0);
|
|
1084
|
+
}
|
|
1085
|
+
const hydrated = await cache.hydrateTimeEntries(entries);
|
|
1086
|
+
const byWorkspace = groupEntriesByWorkspace(hydrated);
|
|
1087
|
+
const summaries = [];
|
|
1088
|
+
byWorkspace.forEach((wsEntries, wsName) => {
|
|
1089
|
+
const wsId = wsEntries[0]?.workspace_id || 0;
|
|
1090
|
+
summaries.push(generateWorkspaceSummary(wsName, wsId, wsEntries));
|
|
1091
|
+
});
|
|
1092
|
+
// Sort by total hours descending
|
|
1093
|
+
summaries.sort((a, b) => b.total_seconds - a.total_seconds);
|
|
1094
|
+
return {
|
|
1095
|
+
content: [
|
|
1096
|
+
{
|
|
1097
|
+
type: 'text',
|
|
1098
|
+
text: JSON.stringify({
|
|
1099
|
+
workspace_count: summaries.length,
|
|
1100
|
+
total_hours: secondsToHours(summaries.reduce((t, s) => t + s.total_seconds, 0)),
|
|
1101
|
+
workspaces: summaries,
|
|
1102
|
+
}, null, 2),
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
// Management tools
|
|
1108
|
+
case 'toggl_list_workspaces': {
|
|
1109
|
+
const workspaces = await cache.getWorkspaces();
|
|
1110
|
+
return {
|
|
1111
|
+
content: [
|
|
1112
|
+
{
|
|
1113
|
+
type: 'text',
|
|
1114
|
+
text: JSON.stringify({
|
|
1115
|
+
count: workspaces.length,
|
|
1116
|
+
workspaces: workspaces.map((ws) => ({
|
|
1117
|
+
id: ws.id,
|
|
1118
|
+
name: ws.name,
|
|
1119
|
+
premium: ws.premium,
|
|
1120
|
+
default_currency: ws.default_currency,
|
|
1121
|
+
})),
|
|
1122
|
+
}, null, 2),
|
|
1123
|
+
},
|
|
1124
|
+
],
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
case 'toggl_list_projects': {
|
|
1128
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'listing projects');
|
|
1129
|
+
const projects = await cache.getProjects(workspaceId);
|
|
1130
|
+
return {
|
|
1131
|
+
content: [
|
|
1132
|
+
{
|
|
1133
|
+
type: 'text',
|
|
1134
|
+
text: JSON.stringify({
|
|
1135
|
+
workspace_id: workspaceId,
|
|
1136
|
+
count: projects.length,
|
|
1137
|
+
projects: projects.map((p) => ({
|
|
1138
|
+
id: p.id,
|
|
1139
|
+
name: p.name,
|
|
1140
|
+
active: p.active,
|
|
1141
|
+
billable: p.billable,
|
|
1142
|
+
color: p.color,
|
|
1143
|
+
client_id: p.client_id,
|
|
1144
|
+
})),
|
|
1145
|
+
}, null, 2),
|
|
1146
|
+
},
|
|
1147
|
+
],
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
case 'toggl_list_clients': {
|
|
1151
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'listing clients');
|
|
1152
|
+
const clients = await cache.getClients(workspaceId);
|
|
1153
|
+
return {
|
|
1154
|
+
content: [
|
|
1155
|
+
{
|
|
1156
|
+
type: 'text',
|
|
1157
|
+
text: JSON.stringify({
|
|
1158
|
+
workspace_id: workspaceId,
|
|
1159
|
+
count: clients.length,
|
|
1160
|
+
clients: clients.map((c) => ({
|
|
1161
|
+
id: c.id,
|
|
1162
|
+
name: c.name,
|
|
1163
|
+
archived: c.archived,
|
|
1164
|
+
})),
|
|
1165
|
+
}, null, 2),
|
|
1166
|
+
},
|
|
1167
|
+
],
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
// Cache management
|
|
1171
|
+
case 'toggl_warm_cache': {
|
|
1172
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'warming the cache');
|
|
1173
|
+
await cache.warmCache(workspaceId);
|
|
1174
|
+
cacheWarmed = true;
|
|
1175
|
+
const stats = cache.getStats();
|
|
1176
|
+
return {
|
|
1177
|
+
content: [
|
|
1178
|
+
{
|
|
1179
|
+
type: 'text',
|
|
1180
|
+
text: JSON.stringify({
|
|
1181
|
+
success: true,
|
|
1182
|
+
message: 'Cache warmed successfully',
|
|
1183
|
+
stats,
|
|
1184
|
+
}, null, 2),
|
|
1185
|
+
},
|
|
1186
|
+
],
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
case 'toggl_cache_stats': {
|
|
1190
|
+
const stats = cache.getStats();
|
|
1191
|
+
const hitRate = stats.hits + stats.misses > 0
|
|
1192
|
+
? Math.round((stats.hits / (stats.hits + stats.misses)) * 100)
|
|
1193
|
+
: 0;
|
|
1194
|
+
return {
|
|
1195
|
+
content: [
|
|
1196
|
+
{
|
|
1197
|
+
type: 'text',
|
|
1198
|
+
text: JSON.stringify({
|
|
1199
|
+
...stats,
|
|
1200
|
+
hit_rate: `${hitRate}%`,
|
|
1201
|
+
cache_warmed: cacheWarmed,
|
|
1202
|
+
}, null, 2),
|
|
1203
|
+
},
|
|
1204
|
+
],
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
case 'toggl_clear_cache': {
|
|
1208
|
+
cache.clearCache();
|
|
1209
|
+
cacheWarmed = false;
|
|
1210
|
+
return {
|
|
1211
|
+
content: [
|
|
1212
|
+
{
|
|
1213
|
+
type: 'text',
|
|
1214
|
+
text: JSON.stringify({
|
|
1215
|
+
success: true,
|
|
1216
|
+
message: 'Cache cleared successfully',
|
|
1217
|
+
}),
|
|
1218
|
+
},
|
|
1219
|
+
],
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
case 'toggl_get_timeline': {
|
|
1223
|
+
localDateRangeFromArgs(args);
|
|
1224
|
+
let allEvents;
|
|
1225
|
+
try {
|
|
1226
|
+
allEvents = await api.getTimeline();
|
|
1227
|
+
}
|
|
1228
|
+
catch (error) {
|
|
1229
|
+
if (error instanceof TimelineNotEnabledError) {
|
|
1230
|
+
return jsonResponse({
|
|
1231
|
+
enabled: false,
|
|
1232
|
+
total_events: 0,
|
|
1233
|
+
returned_events: 0,
|
|
1234
|
+
truncated: false,
|
|
1235
|
+
total_seconds: 0,
|
|
1236
|
+
total_hours: 0,
|
|
1237
|
+
summary: {},
|
|
1238
|
+
events: [],
|
|
1239
|
+
message: 'Toggl Desktop timeline is not enabled yet. Open the Toggl Track Desktop app for Mac, enable timeline/activity tracking and sync, then retry this tool after the app has uploaded activity data.',
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
throw error;
|
|
1243
|
+
}
|
|
1244
|
+
return jsonResponse(buildTimelineResponse(allEvents, args));
|
|
1245
|
+
}
|
|
1246
|
+
default:
|
|
1247
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
catch (error) {
|
|
1251
|
+
return jsonResponse(errorPayload(error));
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
// Start the server
|
|
1255
|
+
async function main() {
|
|
1256
|
+
const transport = new StdioServerTransport();
|
|
1257
|
+
await server.connect(transport);
|
|
1258
|
+
console.error('Toggl MCP server running');
|
|
1259
|
+
}
|
|
1260
|
+
main().catch((error) => {
|
|
1261
|
+
console.error('Server error:', error);
|
|
1262
|
+
process.exit(1);
|
|
1263
|
+
});
|
|
1264
|
+
//# sourceMappingURL=index.js.map
|