@yakirmar/mcp-toggl 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -3
- package/dist/cache-manager.d.ts +2 -0
- package/dist/cache-manager.d.ts.map +1 -1
- package/dist/cache-manager.js +21 -0
- package/dist/cache-manager.js.map +1 -1
- package/dist/index.js +583 -8
- package/dist/index.js.map +1 -1
- package/dist/organization.d.ts +22 -0
- package/dist/organization.d.ts.map +1 -0
- package/dist/organization.js +53 -0
- package/dist/organization.js.map +1 -0
- package/dist/reports.d.ts +5 -0
- package/dist/reports.d.ts.map +1 -0
- package/dist/reports.js +91 -0
- package/dist/reports.js.map +1 -0
- package/dist/toggl-api.d.ts +17 -2
- package/dist/toggl-api.d.ts.map +1 -1
- package/dist/toggl-api.js +104 -17
- package/dist/toggl-api.js.map +1 -1
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +5 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +48 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,22 +5,45 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
|
|
|
5
5
|
import { config } from 'dotenv';
|
|
6
6
|
import { TogglAPI, TimelineNotEnabledError, TogglAPIError } from './toggl-api.js';
|
|
7
7
|
import { buildTimelineResponse } from './timeline.js';
|
|
8
|
+
import { normalizeReportRows, summarizeByUser, userDisplayName } from './reports.js';
|
|
8
9
|
import { CacheManager } from './cache-manager.js';
|
|
9
10
|
import { WorkspaceResolutionError, parseWorkspaceId, resolveWorkspaceId, resolveProjectForClient, } from './workspace.js';
|
|
10
|
-
import {
|
|
11
|
+
import { OrganizationResolutionError, parseOrganizationId, resolveOrganizationId, } from './organization.js';
|
|
12
|
+
import { buildTimeEntryInterval, filterHydratedEntries, pickDefined, getDateRange, generateDailyReport, generateWeeklyReport, formatReportForDisplay, secondsToHours, groupEntriesByProject, groupEntriesByWorkspace, generateProjectSummary, generateWorkspaceSummary, toLocalYMD, parseLocalYMD, localDateRangeFromArgs, reportDateWindow, } from './utils.js';
|
|
11
13
|
function parseInclusiveEndDate(value) {
|
|
12
14
|
const date = parseLocalYMD(value);
|
|
13
15
|
date.setDate(date.getDate() + 1);
|
|
14
16
|
return date;
|
|
15
17
|
}
|
|
16
18
|
// Parse a required positive-integer entity id from tool arguments.
|
|
17
|
-
function
|
|
19
|
+
function requireId(value, field) {
|
|
18
20
|
const parsed = typeof value === 'number' ? value : Number(value);
|
|
19
21
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
20
|
-
throw new Error(
|
|
22
|
+
throw new Error(`${field} is required and must be a positive integer.`);
|
|
21
23
|
}
|
|
22
24
|
return parsed;
|
|
23
25
|
}
|
|
26
|
+
function requireName(value, field) {
|
|
27
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
28
|
+
throw new Error(`${field} is required and must be a non-empty string.`);
|
|
29
|
+
}
|
|
30
|
+
return value.trim();
|
|
31
|
+
}
|
|
32
|
+
// Fields a caller may set when creating or updating a project / client.
|
|
33
|
+
const PROJECT_FIELDS = [
|
|
34
|
+
'name',
|
|
35
|
+
'client_id',
|
|
36
|
+
'active',
|
|
37
|
+
'is_private',
|
|
38
|
+
'billable',
|
|
39
|
+
'color',
|
|
40
|
+
'estimated_hours',
|
|
41
|
+
'start_date',
|
|
42
|
+
'end_date',
|
|
43
|
+
'currency',
|
|
44
|
+
'rate',
|
|
45
|
+
];
|
|
46
|
+
const CLIENT_FIELDS = ['name', 'notes', 'archived'];
|
|
24
47
|
function jsonResponse(data) {
|
|
25
48
|
return {
|
|
26
49
|
content: [
|
|
@@ -54,10 +77,15 @@ function errorPayload(error) {
|
|
|
54
77
|
payload.tip = error.tip;
|
|
55
78
|
payload.available_workspaces = error.available_workspaces;
|
|
56
79
|
}
|
|
80
|
+
if (error instanceof OrganizationResolutionError) {
|
|
81
|
+
payload.code = error.code;
|
|
82
|
+
payload.tip = error.tip;
|
|
83
|
+
payload.available_organizations = error.available_organizations;
|
|
84
|
+
}
|
|
57
85
|
return payload;
|
|
58
86
|
}
|
|
59
87
|
// Version for CLI output and server metadata
|
|
60
|
-
const VERSION = '1.
|
|
88
|
+
const VERSION = '1.2.0';
|
|
61
89
|
// Basic CLI flags: --help / -h and --version / -v
|
|
62
90
|
const argv = process.argv.slice(2);
|
|
63
91
|
if (argv.includes('--version') || argv.includes('-v')) {
|
|
@@ -67,7 +95,7 @@ if (argv.includes('--version') || argv.includes('-v')) {
|
|
|
67
95
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
68
96
|
console.error(`mcp-toggl - Toggl MCP Server\n\n` +
|
|
69
97
|
`Usage:\n` +
|
|
70
|
-
` npx @
|
|
98
|
+
` npx @yakirmar/mcp-toggl@latest [--help] [--version]\n\n` +
|
|
71
99
|
`Environment:\n` +
|
|
72
100
|
` TOGGL_API_KEY Required Toggl API token\n` +
|
|
73
101
|
` TOGGL_DEFAULT_WORKSPACE_ID Optional default workspace id\n` +
|
|
@@ -77,7 +105,7 @@ if (argv.includes('--help') || argv.includes('-h')) {
|
|
|
77
105
|
` {\n` +
|
|
78
106
|
` "mcpServers": {\n` +
|
|
79
107
|
` "mcp-toggl": {\n` +
|
|
80
|
-
` "command": "npx @
|
|
108
|
+
` "command": "npx @yakirmar/mcp-toggl@latest",\n` +
|
|
81
109
|
` "env": { "TOGGL_API_KEY": "your_api_key_here" }\n` +
|
|
82
110
|
` }\n` +
|
|
83
111
|
` }\n` +
|
|
@@ -88,7 +116,7 @@ if (argv.includes('--help') || argv.includes('-h')) {
|
|
|
88
116
|
` "servers": {\n` +
|
|
89
117
|
` "mcp-toggl": {\n` +
|
|
90
118
|
` "command": "npx",\n` +
|
|
91
|
-
` "args": ["@
|
|
119
|
+
` "args": ["@yakirmar/mcp-toggl@latest"],\n` +
|
|
92
120
|
` "env": { "TOGGL_API_KEY": "your_api_key_here" }\n` +
|
|
93
121
|
` }\n` +
|
|
94
122
|
` }\n` +
|
|
@@ -117,6 +145,7 @@ const cacheConfig = {
|
|
|
117
145
|
batchSize: parseInt(process.env.TOGGL_BATCH_SIZE || '100'),
|
|
118
146
|
};
|
|
119
147
|
const defaultWorkspaceId = parseWorkspaceId(process.env.TOGGL_DEFAULT_WORKSPACE_ID);
|
|
148
|
+
const defaultOrganizationId = parseOrganizationId(process.env.TOGGL_DEFAULT_ORG_ID);
|
|
120
149
|
// Initialize API and cache
|
|
121
150
|
const api = new TogglAPI(API_KEY);
|
|
122
151
|
const cache = new CacheManager(cacheConfig);
|
|
@@ -148,6 +177,71 @@ async function resolveWorkspaceForTool(args, action) {
|
|
|
148
177
|
action,
|
|
149
178
|
});
|
|
150
179
|
}
|
|
180
|
+
async function resolveOrganizationForTool(args, action) {
|
|
181
|
+
return resolveOrganizationId({
|
|
182
|
+
explicitOrganizationId: args?.organization_id,
|
|
183
|
+
defaultOrganizationId,
|
|
184
|
+
getWorkspaces: () => cache.getWorkspaces(),
|
|
185
|
+
action,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Workspace member id -> display name. Requires admin rights; when the token
|
|
189
|
+
// lacks them we degrade to the username Toggl embeds in each report row rather
|
|
190
|
+
// than failing the whole call.
|
|
191
|
+
async function workspaceUserNames(workspaceId) {
|
|
192
|
+
try {
|
|
193
|
+
const users = await api.getWorkspaceUsers(workspaceId);
|
|
194
|
+
return new Map(users.map((user) => [user.id, userDisplayName(user)]));
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
console.error(`Could not list users for workspace ${workspaceId} (admin rights required):`, error);
|
|
198
|
+
return new Map();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const REPORT_FILTER_FIELDS = [
|
|
202
|
+
'user_ids',
|
|
203
|
+
'project_ids',
|
|
204
|
+
'client_ids',
|
|
205
|
+
'tag_ids',
|
|
206
|
+
'task_ids',
|
|
207
|
+
'billable',
|
|
208
|
+
'description',
|
|
209
|
+
];
|
|
210
|
+
// Shared path for both team tools: pull the cross-user detailed report and the
|
|
211
|
+
// reference data needed to make sense of the ids inside it. The rows themselves
|
|
212
|
+
// are left untouched — the Reports API response schema is not published.
|
|
213
|
+
async function fetchTeamReport(workspaceId, args) {
|
|
214
|
+
const window = reportDateWindow(args);
|
|
215
|
+
const params = {
|
|
216
|
+
...pickDefined(args ?? {}, REPORT_FILTER_FIELDS),
|
|
217
|
+
start_date: window.start_date,
|
|
218
|
+
end_date: window.end_date,
|
|
219
|
+
};
|
|
220
|
+
const minMinutes = args?.min_duration_minutes;
|
|
221
|
+
if (typeof minMinutes === 'number')
|
|
222
|
+
params.min_duration_seconds = Math.round(minMinutes * 60);
|
|
223
|
+
const maxMinutes = args?.max_duration_minutes;
|
|
224
|
+
if (typeof maxMinutes === 'number')
|
|
225
|
+
params.max_duration_seconds = Math.round(maxMinutes * 60);
|
|
226
|
+
const [{ rows, truncated }, userNames, projects, clients] = await Promise.all([
|
|
227
|
+
api.searchDetailedReport(workspaceId, params),
|
|
228
|
+
workspaceUserNames(workspaceId),
|
|
229
|
+
cache.getProjects(workspaceId),
|
|
230
|
+
cache.getClients(workspaceId),
|
|
231
|
+
]);
|
|
232
|
+
return {
|
|
233
|
+
rows,
|
|
234
|
+
window,
|
|
235
|
+
truncated,
|
|
236
|
+
users: [...userNames].map(([id, name]) => ({ id, name })),
|
|
237
|
+
projects: projects.map((project) => ({
|
|
238
|
+
id: project.id,
|
|
239
|
+
name: project.name,
|
|
240
|
+
client_id: project.client_id,
|
|
241
|
+
})),
|
|
242
|
+
clients: clients.map((client) => ({ id: client.id, name: client.name })),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
151
245
|
// Create MCP server
|
|
152
246
|
const server = new Server({
|
|
153
247
|
name: 'mcp-toggl',
|
|
@@ -598,6 +692,326 @@ const tools = [
|
|
|
598
692
|
},
|
|
599
693
|
},
|
|
600
694
|
},
|
|
695
|
+
{
|
|
696
|
+
name: 'toggl_create_project',
|
|
697
|
+
description: 'Create a project in a workspace. Only name is required; optionally attach it to a client and set billing, color, dates, and rate.',
|
|
698
|
+
annotations: {
|
|
699
|
+
readOnlyHint: false,
|
|
700
|
+
idempotentHint: false,
|
|
701
|
+
openWorldHint: true,
|
|
702
|
+
},
|
|
703
|
+
inputSchema: {
|
|
704
|
+
type: 'object',
|
|
705
|
+
properties: {
|
|
706
|
+
name: { type: 'string', description: 'Project name (required).' },
|
|
707
|
+
workspace_id: {
|
|
708
|
+
type: 'number',
|
|
709
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
710
|
+
},
|
|
711
|
+
client_id: { type: 'number', description: 'Client to attach the project to (optional).' },
|
|
712
|
+
active: { type: 'boolean', description: 'Whether the project is active (default: true).' },
|
|
713
|
+
is_private: { type: 'boolean', description: 'Whether the project is private.' },
|
|
714
|
+
billable: { type: 'boolean', description: 'Whether the project is billable.' },
|
|
715
|
+
color: { type: 'string', description: 'Project color as a hex string, e.g. "#0b83d9".' },
|
|
716
|
+
estimated_hours: { type: 'number', description: 'Estimated hours for the project.' },
|
|
717
|
+
start_date: { type: 'string', description: 'Project start date (YYYY-MM-DD).' },
|
|
718
|
+
end_date: { type: 'string', description: 'Project end date (YYYY-MM-DD).' },
|
|
719
|
+
currency: { type: 'string', description: 'Currency code, e.g. "USD".' },
|
|
720
|
+
rate: { type: 'number', description: 'Hourly rate for the project.' },
|
|
721
|
+
},
|
|
722
|
+
required: ['name'],
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
name: 'toggl_update_project',
|
|
727
|
+
description: 'Update an existing project. Provide project_id plus at least one field to change; omitted fields are left untouched.',
|
|
728
|
+
annotations: {
|
|
729
|
+
readOnlyHint: false,
|
|
730
|
+
idempotentHint: true,
|
|
731
|
+
openWorldHint: true,
|
|
732
|
+
},
|
|
733
|
+
inputSchema: {
|
|
734
|
+
type: 'object',
|
|
735
|
+
properties: {
|
|
736
|
+
project_id: { type: 'number', description: 'Project to update (required).' },
|
|
737
|
+
workspace_id: {
|
|
738
|
+
type: 'number',
|
|
739
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
740
|
+
},
|
|
741
|
+
name: { type: 'string', description: 'New project name.' },
|
|
742
|
+
client_id: {
|
|
743
|
+
type: 'number',
|
|
744
|
+
description: 'Move the project to this client.',
|
|
745
|
+
},
|
|
746
|
+
active: {
|
|
747
|
+
type: 'boolean',
|
|
748
|
+
description: 'Set false to archive the project, true to reactivate it.',
|
|
749
|
+
},
|
|
750
|
+
is_private: { type: 'boolean', description: 'Whether the project is private.' },
|
|
751
|
+
billable: { type: 'boolean', description: 'Whether the project is billable.' },
|
|
752
|
+
color: { type: 'string', description: 'Project color as a hex string.' },
|
|
753
|
+
estimated_hours: { type: 'number', description: 'Estimated hours for the project.' },
|
|
754
|
+
start_date: { type: 'string', description: 'Project start date (YYYY-MM-DD).' },
|
|
755
|
+
end_date: { type: 'string', description: 'Project end date (YYYY-MM-DD).' },
|
|
756
|
+
currency: { type: 'string', description: 'Currency code.' },
|
|
757
|
+
rate: { type: 'number', description: 'Hourly rate for the project.' },
|
|
758
|
+
},
|
|
759
|
+
required: ['project_id'],
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
name: 'toggl_delete_project',
|
|
764
|
+
description: 'Delete a project by id. This cannot be undone; time entries on the project lose their project association. To keep history, prefer toggl_update_project with active: false to archive it instead.',
|
|
765
|
+
annotations: {
|
|
766
|
+
readOnlyHint: false,
|
|
767
|
+
idempotentHint: false,
|
|
768
|
+
destructiveHint: true,
|
|
769
|
+
openWorldHint: true,
|
|
770
|
+
},
|
|
771
|
+
inputSchema: {
|
|
772
|
+
type: 'object',
|
|
773
|
+
properties: {
|
|
774
|
+
project_id: { type: 'number', description: 'Project to delete (required).' },
|
|
775
|
+
workspace_id: {
|
|
776
|
+
type: 'number',
|
|
777
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
required: ['project_id'],
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: 'toggl_create_client',
|
|
785
|
+
description: 'Create a client in a workspace.',
|
|
786
|
+
annotations: {
|
|
787
|
+
readOnlyHint: false,
|
|
788
|
+
idempotentHint: false,
|
|
789
|
+
openWorldHint: true,
|
|
790
|
+
},
|
|
791
|
+
inputSchema: {
|
|
792
|
+
type: 'object',
|
|
793
|
+
properties: {
|
|
794
|
+
name: { type: 'string', description: 'Client name (required).' },
|
|
795
|
+
workspace_id: {
|
|
796
|
+
type: 'number',
|
|
797
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
798
|
+
},
|
|
799
|
+
notes: { type: 'string', description: 'Free-form notes for the client (optional).' },
|
|
800
|
+
},
|
|
801
|
+
required: ['name'],
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
name: 'toggl_update_client',
|
|
806
|
+
description: 'Update an existing client. Provide client_id plus at least one field to change; omitted fields are left untouched. Set archived: true to archive.',
|
|
807
|
+
annotations: {
|
|
808
|
+
readOnlyHint: false,
|
|
809
|
+
idempotentHint: true,
|
|
810
|
+
openWorldHint: true,
|
|
811
|
+
},
|
|
812
|
+
inputSchema: {
|
|
813
|
+
type: 'object',
|
|
814
|
+
properties: {
|
|
815
|
+
client_id: { type: 'number', description: 'Client to update (required).' },
|
|
816
|
+
workspace_id: {
|
|
817
|
+
type: 'number',
|
|
818
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
819
|
+
},
|
|
820
|
+
name: { type: 'string', description: 'New client name.' },
|
|
821
|
+
notes: { type: 'string', description: 'Free-form notes for the client.' },
|
|
822
|
+
archived: { type: 'boolean', description: 'Archive (true) or unarchive (false).' },
|
|
823
|
+
},
|
|
824
|
+
required: ['client_id'],
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
name: 'toggl_delete_client',
|
|
829
|
+
description: 'Delete a client by id. This cannot be undone; projects belonging to the client lose their client association. To keep history, prefer toggl_update_client with archived: true instead.',
|
|
830
|
+
annotations: {
|
|
831
|
+
readOnlyHint: false,
|
|
832
|
+
idempotentHint: false,
|
|
833
|
+
destructiveHint: true,
|
|
834
|
+
openWorldHint: true,
|
|
835
|
+
},
|
|
836
|
+
inputSchema: {
|
|
837
|
+
type: 'object',
|
|
838
|
+
properties: {
|
|
839
|
+
client_id: { type: 'number', description: 'Client to delete (required).' },
|
|
840
|
+
workspace_id: {
|
|
841
|
+
type: 'number',
|
|
842
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
required: ['client_id'],
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
// Team / admin tools (workspace-wide, require admin rights)
|
|
849
|
+
{
|
|
850
|
+
name: 'toggl_list_users',
|
|
851
|
+
description: 'List the members of a workspace (id, name, email, admin/owner flags). ADMIN ONLY: requires admin rights on the workspace; a non-admin token typically gets a 403. Use the returned ids as user_ids for toggl_team_entries / toggl_team_summary.',
|
|
852
|
+
annotations: {
|
|
853
|
+
readOnlyHint: true,
|
|
854
|
+
idempotentHint: true,
|
|
855
|
+
openWorldHint: true,
|
|
856
|
+
},
|
|
857
|
+
inputSchema: {
|
|
858
|
+
type: 'object',
|
|
859
|
+
properties: {
|
|
860
|
+
workspace_id: {
|
|
861
|
+
type: 'number',
|
|
862
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
name: 'toggl_list_org_users',
|
|
869
|
+
description: 'List the members of an ORGANIZATION (across all its workspaces), with richer data than toggl_list_users: org/workspace admin flags, active status, role, and how many workspaces each member belongs to. ORG ADMIN ONLY. If organization_id is omitted it is derived from your workspaces (or TOGGL_DEFAULT_ORG_ID). Use the returned user_id values as user_ids for toggl_team_entries / toggl_team_summary. Prefer toggl_list_users when you only care about one workspace.',
|
|
870
|
+
annotations: {
|
|
871
|
+
readOnlyHint: true,
|
|
872
|
+
idempotentHint: true,
|
|
873
|
+
openWorldHint: true,
|
|
874
|
+
},
|
|
875
|
+
inputSchema: {
|
|
876
|
+
type: 'object',
|
|
877
|
+
properties: {
|
|
878
|
+
organization_id: {
|
|
879
|
+
type: 'number',
|
|
880
|
+
description: 'Organization ID. If omitted, uses TOGGL_DEFAULT_ORG_ID or the only organization derivable from your workspaces.',
|
|
881
|
+
},
|
|
882
|
+
filter: {
|
|
883
|
+
type: 'string',
|
|
884
|
+
description: 'Free-text filter on name/email, applied by Toggl.',
|
|
885
|
+
},
|
|
886
|
+
active_status: {
|
|
887
|
+
type: 'string',
|
|
888
|
+
description: 'Filter by active status as supported by Toggl, e.g. "active" or "inactive".',
|
|
889
|
+
},
|
|
890
|
+
only_admins: {
|
|
891
|
+
type: 'boolean',
|
|
892
|
+
description: 'Return only organization admins.',
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
name: 'toggl_team_entries',
|
|
899
|
+
description: "Get time entries for OTHER users in the workspace (what your team worked on), via the Toggl Reports API. ADMIN ONLY: what you can see is enforced by Toggl — a non-admin token sees only its own data. PRIVACY: this returns teammates' entry descriptions. Filter with user_ids (from toggl_list_users), project_ids, client_ids, tag_ids, billable, or description. Date window via period or start_date/end_date (INCLUSIVE; defaults to the last 31 days). Entries are hydrated with user/project/client names and sorted newest-first.",
|
|
900
|
+
annotations: {
|
|
901
|
+
readOnlyHint: true,
|
|
902
|
+
idempotentHint: true,
|
|
903
|
+
openWorldHint: true,
|
|
904
|
+
},
|
|
905
|
+
inputSchema: {
|
|
906
|
+
type: 'object',
|
|
907
|
+
properties: {
|
|
908
|
+
workspace_id: {
|
|
909
|
+
type: 'number',
|
|
910
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
911
|
+
},
|
|
912
|
+
user_ids: {
|
|
913
|
+
type: 'array',
|
|
914
|
+
items: { type: 'number' },
|
|
915
|
+
description: 'Only entries by these user ids. Omit for all visible users.',
|
|
916
|
+
},
|
|
917
|
+
project_ids: {
|
|
918
|
+
type: 'array',
|
|
919
|
+
items: { type: 'number' },
|
|
920
|
+
description: 'Only entries on these project ids.',
|
|
921
|
+
},
|
|
922
|
+
client_ids: {
|
|
923
|
+
type: 'array',
|
|
924
|
+
items: { type: 'number' },
|
|
925
|
+
description: 'Only entries for these client ids.',
|
|
926
|
+
},
|
|
927
|
+
tag_ids: {
|
|
928
|
+
type: 'array',
|
|
929
|
+
items: { type: 'number' },
|
|
930
|
+
description: 'Only entries carrying these tag ids.',
|
|
931
|
+
},
|
|
932
|
+
description: {
|
|
933
|
+
type: 'string',
|
|
934
|
+
description: 'Filter by entry description (matched by Toggl).',
|
|
935
|
+
},
|
|
936
|
+
billable: { type: 'boolean', description: 'Only billable (true) or non-billable (false).' },
|
|
937
|
+
period: {
|
|
938
|
+
type: 'string',
|
|
939
|
+
enum: ['today', 'yesterday', 'week', 'lastWeek', 'month', 'lastMonth'],
|
|
940
|
+
description: 'Predefined date window (alternative to start_date/end_date).',
|
|
941
|
+
},
|
|
942
|
+
start_date: {
|
|
943
|
+
type: 'string',
|
|
944
|
+
description: 'Window start (YYYY-MM-DD, inclusive, local timezone).',
|
|
945
|
+
},
|
|
946
|
+
end_date: {
|
|
947
|
+
type: 'string',
|
|
948
|
+
description: 'Window end (YYYY-MM-DD, inclusive, local timezone).',
|
|
949
|
+
},
|
|
950
|
+
min_duration_minutes: {
|
|
951
|
+
type: 'number',
|
|
952
|
+
description: 'Only entries at least this many minutes long.',
|
|
953
|
+
},
|
|
954
|
+
max_duration_minutes: {
|
|
955
|
+
type: 'number',
|
|
956
|
+
description: 'Only entries at most this many minutes long.',
|
|
957
|
+
},
|
|
958
|
+
limit: {
|
|
959
|
+
type: 'number',
|
|
960
|
+
minimum: 1,
|
|
961
|
+
maximum: 1000,
|
|
962
|
+
default: 100,
|
|
963
|
+
description: 'Maximum entries to return (default: 100, max: 1000).',
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
name: 'toggl_team_summary',
|
|
970
|
+
description: 'Total hours per user across the workspace for a period — who logged how much, how much was billable, and across how many projects. ADMIN ONLY: requires admin rights; a non-admin token sees only its own totals. Accepts the same filters and date window as toggl_team_entries (dates INCLUSIVE; defaults to the last 31 days). Sorted by total hours descending.',
|
|
971
|
+
annotations: {
|
|
972
|
+
readOnlyHint: true,
|
|
973
|
+
idempotentHint: true,
|
|
974
|
+
openWorldHint: true,
|
|
975
|
+
},
|
|
976
|
+
inputSchema: {
|
|
977
|
+
type: 'object',
|
|
978
|
+
properties: {
|
|
979
|
+
workspace_id: {
|
|
980
|
+
type: 'number',
|
|
981
|
+
description: 'Workspace ID. If omitted, uses TOGGL_DEFAULT_WORKSPACE_ID or the only available workspace; required when multiple workspaces exist.',
|
|
982
|
+
},
|
|
983
|
+
user_ids: {
|
|
984
|
+
type: 'array',
|
|
985
|
+
items: { type: 'number' },
|
|
986
|
+
description: 'Only these user ids. Omit for all visible users.',
|
|
987
|
+
},
|
|
988
|
+
project_ids: {
|
|
989
|
+
type: 'array',
|
|
990
|
+
items: { type: 'number' },
|
|
991
|
+
description: 'Only entries on these project ids.',
|
|
992
|
+
},
|
|
993
|
+
client_ids: {
|
|
994
|
+
type: 'array',
|
|
995
|
+
items: { type: 'number' },
|
|
996
|
+
description: 'Only entries for these client ids.',
|
|
997
|
+
},
|
|
998
|
+
billable: { type: 'boolean', description: 'Only billable (true) or non-billable (false).' },
|
|
999
|
+
period: {
|
|
1000
|
+
type: 'string',
|
|
1001
|
+
enum: ['today', 'yesterday', 'week', 'lastWeek', 'month', 'lastMonth'],
|
|
1002
|
+
description: 'Predefined date window (alternative to start_date/end_date).',
|
|
1003
|
+
},
|
|
1004
|
+
start_date: {
|
|
1005
|
+
type: 'string',
|
|
1006
|
+
description: 'Window start (YYYY-MM-DD, inclusive, local timezone).',
|
|
1007
|
+
},
|
|
1008
|
+
end_date: {
|
|
1009
|
+
type: 'string',
|
|
1010
|
+
description: 'Window end (YYYY-MM-DD, inclusive, local timezone).',
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
601
1015
|
// Cache management
|
|
602
1016
|
{
|
|
603
1017
|
name: 'toggl_warm_cache',
|
|
@@ -939,7 +1353,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
939
1353
|
});
|
|
940
1354
|
}
|
|
941
1355
|
case 'toggl_delete_entry': {
|
|
942
|
-
const timeEntryId =
|
|
1356
|
+
const timeEntryId = requireId(args?.time_entry_id, 'time_entry_id');
|
|
943
1357
|
let workspaceId = parseWorkspaceId(args?.workspace_id);
|
|
944
1358
|
if (workspaceId === undefined) {
|
|
945
1359
|
let existing;
|
|
@@ -1167,6 +1581,167 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1167
1581
|
],
|
|
1168
1582
|
};
|
|
1169
1583
|
}
|
|
1584
|
+
case 'toggl_create_project': {
|
|
1585
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'creating a project');
|
|
1586
|
+
const name = requireName(args?.name, 'name');
|
|
1587
|
+
const payload = {
|
|
1588
|
+
...pickDefined(args ?? {}, PROJECT_FIELDS),
|
|
1589
|
+
name,
|
|
1590
|
+
};
|
|
1591
|
+
const project = await api.createProject(workspaceId, payload);
|
|
1592
|
+
cache.invalidateProjects(workspaceId);
|
|
1593
|
+
return jsonResponse({
|
|
1594
|
+
success: true,
|
|
1595
|
+
message: 'Project created',
|
|
1596
|
+
project,
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
case 'toggl_update_project': {
|
|
1600
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'updating a project');
|
|
1601
|
+
const projectId = requireId(args?.project_id, 'project_id');
|
|
1602
|
+
const updates = pickDefined(args ?? {}, PROJECT_FIELDS);
|
|
1603
|
+
if (Object.keys(updates).length === 0) {
|
|
1604
|
+
throw new Error(`Provide at least one field to update. Updatable fields: ${PROJECT_FIELDS.join(', ')}.`);
|
|
1605
|
+
}
|
|
1606
|
+
const project = await api.updateProject(workspaceId, projectId, updates);
|
|
1607
|
+
cache.invalidateProjects(workspaceId);
|
|
1608
|
+
return jsonResponse({
|
|
1609
|
+
success: true,
|
|
1610
|
+
message: 'Project updated',
|
|
1611
|
+
project,
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
case 'toggl_delete_project': {
|
|
1615
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'deleting a project');
|
|
1616
|
+
const projectId = requireId(args?.project_id, 'project_id');
|
|
1617
|
+
await api.deleteProject(workspaceId, projectId);
|
|
1618
|
+
cache.invalidateProjects(workspaceId);
|
|
1619
|
+
return jsonResponse({
|
|
1620
|
+
success: true,
|
|
1621
|
+
message: 'Project deleted',
|
|
1622
|
+
project_id: projectId,
|
|
1623
|
+
workspace_id: workspaceId,
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
case 'toggl_create_client': {
|
|
1627
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'creating a client');
|
|
1628
|
+
const name = requireName(args?.name, 'name');
|
|
1629
|
+
const payload = {
|
|
1630
|
+
...pickDefined(args ?? {}, ['name', 'notes']),
|
|
1631
|
+
name,
|
|
1632
|
+
};
|
|
1633
|
+
const client = await api.createClient(workspaceId, payload);
|
|
1634
|
+
cache.invalidateClients(workspaceId);
|
|
1635
|
+
return jsonResponse({
|
|
1636
|
+
success: true,
|
|
1637
|
+
message: 'Client created',
|
|
1638
|
+
client,
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
case 'toggl_update_client': {
|
|
1642
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'updating a client');
|
|
1643
|
+
const clientId = requireId(args?.client_id, 'client_id');
|
|
1644
|
+
const updates = pickDefined(args ?? {}, CLIENT_FIELDS);
|
|
1645
|
+
if (Object.keys(updates).length === 0) {
|
|
1646
|
+
throw new Error(`Provide at least one field to update. Updatable fields: ${CLIENT_FIELDS.join(', ')}.`);
|
|
1647
|
+
}
|
|
1648
|
+
const client = await api.updateClient(workspaceId, clientId, updates);
|
|
1649
|
+
cache.invalidateClients(workspaceId);
|
|
1650
|
+
return jsonResponse({
|
|
1651
|
+
success: true,
|
|
1652
|
+
message: 'Client updated',
|
|
1653
|
+
client,
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
case 'toggl_delete_client': {
|
|
1657
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'deleting a client');
|
|
1658
|
+
const clientId = requireId(args?.client_id, 'client_id');
|
|
1659
|
+
await api.deleteClient(workspaceId, clientId);
|
|
1660
|
+
cache.invalidateClients(workspaceId);
|
|
1661
|
+
return jsonResponse({
|
|
1662
|
+
success: true,
|
|
1663
|
+
message: 'Client deleted',
|
|
1664
|
+
client_id: clientId,
|
|
1665
|
+
workspace_id: workspaceId,
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
// Team / admin tools
|
|
1669
|
+
case 'toggl_list_users': {
|
|
1670
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'listing workspace users');
|
|
1671
|
+
const users = await api.getWorkspaceUsers(workspaceId);
|
|
1672
|
+
// Passed through exactly as Toggl returns them: this endpoint's response
|
|
1673
|
+
// schema is not published, so reshaping it here risks dropping or
|
|
1674
|
+
// mislabeling fields. Interpret the objects as-is.
|
|
1675
|
+
return jsonResponse({
|
|
1676
|
+
workspace_id: workspaceId,
|
|
1677
|
+
count: users.length,
|
|
1678
|
+
note: 'users[] is Toggl\'s raw response, unmodified. The Toggl user id (usually "id" here) is what time entries are keyed by — pass it as user_ids to toggl_team_entries / toggl_team_summary.',
|
|
1679
|
+
users,
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
case 'toggl_list_org_users': {
|
|
1683
|
+
const organizationId = await resolveOrganizationForTool(args, 'listing organization users');
|
|
1684
|
+
const users = await api.getOrganizationUsers(organizationId, {
|
|
1685
|
+
filter: args?.filter,
|
|
1686
|
+
active_status: args?.active_status,
|
|
1687
|
+
only_admins: args?.only_admins,
|
|
1688
|
+
});
|
|
1689
|
+
// Raw passthrough — this endpoint's response schema is not published.
|
|
1690
|
+
return jsonResponse({
|
|
1691
|
+
organization_id: organizationId,
|
|
1692
|
+
count: users.length,
|
|
1693
|
+
note: "users[] is Toggl's raw response, unmodified. CAUTION: these objects carry two different ids — the organization-user id (\"id\") and the Toggl user id (\"user_id\"). Only user_id matches time entries, so pass user_id (not id) as user_ids to toggl_team_entries / toggl_team_summary.",
|
|
1694
|
+
users,
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
case 'toggl_team_entries': {
|
|
1698
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'reading team time entries');
|
|
1699
|
+
const { rows, window, truncated, users, projects, clients } = await fetchTeamReport(workspaceId, args);
|
|
1700
|
+
const requestedLimit = typeof args?.limit === 'number' ? args.limit : 100;
|
|
1701
|
+
const limit = Math.min(Math.max(1, Math.floor(requestedLimit)), 1000);
|
|
1702
|
+
const limited = rows.slice(0, limit);
|
|
1703
|
+
// Rows are returned exactly as the Reports API produced them — its response
|
|
1704
|
+
// schema is not published, so reshaping risks dropping fields. Reference
|
|
1705
|
+
// lookups are supplied alongside so ids can be resolved to names.
|
|
1706
|
+
return jsonResponse({
|
|
1707
|
+
workspace_id: workspaceId,
|
|
1708
|
+
start_date: window.start_date,
|
|
1709
|
+
end_date: window.end_date,
|
|
1710
|
+
row_count: rows.length,
|
|
1711
|
+
returned: limited.length,
|
|
1712
|
+
truncated: truncated || rows.length > limited.length,
|
|
1713
|
+
note: "rows[] is the Toggl Reports API response, unmodified. A row typically groups several time entries under a nested array. Use the lookups below to resolve user_id / project_id / client_id to names. Durations are in seconds.",
|
|
1714
|
+
lookups: { users, projects, clients },
|
|
1715
|
+
rows: limited,
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
case 'toggl_team_summary': {
|
|
1719
|
+
const workspaceId = await resolveWorkspaceForTool(args, 'summarizing team time');
|
|
1720
|
+
const { rows, window, truncated, users: workspaceUsers } = await fetchTeamReport(workspaceId, args);
|
|
1721
|
+
// Unlike the other team tools this cannot be a raw passthrough: totalling
|
|
1722
|
+
// hours per user requires knowing which field holds the duration.
|
|
1723
|
+
const userNames = new Map(workspaceUsers.map((user) => [user.id, user.name]));
|
|
1724
|
+
const entries = normalizeReportRows(rows, userNames);
|
|
1725
|
+
const users = summarizeByUser(entries);
|
|
1726
|
+
const totalSeconds = users.reduce((sum, user) => sum + user.total_seconds, 0);
|
|
1727
|
+
// If Toggl returned rows but every duration came out zero, the field names
|
|
1728
|
+
// this aggregation assumes are wrong. Say so instead of reporting "0 hours"
|
|
1729
|
+
// as though the team logged nothing.
|
|
1730
|
+
const schemaWarning = rows.length > 0 && totalSeconds === 0
|
|
1731
|
+
? 'Toggl returned rows but no durations could be read from them, so these totals are unreliable. The report response shape may differ from what this aggregation expects. Use toggl_team_entries to inspect the raw rows.'
|
|
1732
|
+
: undefined;
|
|
1733
|
+
return jsonResponse({
|
|
1734
|
+
workspace_id: workspaceId,
|
|
1735
|
+
start_date: window.start_date,
|
|
1736
|
+
end_date: window.end_date,
|
|
1737
|
+
user_count: users.length,
|
|
1738
|
+
total_hours: secondsToHours(totalSeconds),
|
|
1739
|
+
total_seconds: totalSeconds,
|
|
1740
|
+
truncated,
|
|
1741
|
+
...(schemaWarning ? { schema_warning: schemaWarning } : {}),
|
|
1742
|
+
users,
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1170
1745
|
// Cache management
|
|
1171
1746
|
case 'toggl_warm_cache': {
|
|
1172
1747
|
const workspaceId = await resolveWorkspaceForTool(args, 'warming the cache');
|