clokio-mcp 0.27.1 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/attendance.js +56 -14
- package/package.json +1 -1
package/dist/tools/attendance.js
CHANGED
|
@@ -54,9 +54,20 @@ export function entryClock(e, opts = {}) {
|
|
|
54
54
|
* The zone is the entry's `timezone` (local times) or UTC (an older server).
|
|
55
55
|
* `zone: false` drops a local entry's zone, for a page whose header names it once.
|
|
56
56
|
*/
|
|
57
|
+
/**
|
|
58
|
+
* What an entry is on, in words: its task, else its project (task #5775: an
|
|
59
|
+
* entry may be on a project with no task), else nothing. ONE copy for every
|
|
60
|
+
* line that names it (timeEntryLine, clokio_delete_time_entry), so they
|
|
61
|
+
* cannot drift (MCP QA 0.28.0).
|
|
62
|
+
*/
|
|
63
|
+
export function entryTarget(e) {
|
|
64
|
+
if (e?.task)
|
|
65
|
+
return `task #${e.task.id} ${e.task.title}`;
|
|
66
|
+
return e?.project?.name ? `no task, project ${e.project.name}` : 'no task';
|
|
67
|
+
}
|
|
57
68
|
export function timeEntryLine(e, opts = {}) {
|
|
58
69
|
const id = e?.id === undefined || e?.id === null ? '(no id: server older than 2026-10-16)' : `#${e.id}`;
|
|
59
|
-
const task = e
|
|
70
|
+
const task = entryTarget(e);
|
|
60
71
|
return `${id} ${e?.date ?? '-'} ${entryClock(e, opts)} ${e?.work_hour ?? 0}h | ${e?.employee_code ?? '-'} | ${task} | ${e?.description ?? ''}`;
|
|
61
72
|
}
|
|
62
73
|
const ZONE_NOTE = "the person's CURRENT location zone, which clokio_update_time_entry reads date/start/end in (after a location change it may differ from where the entry was logged)";
|
|
@@ -208,6 +219,21 @@ export function withShortEntryWarning(res) {
|
|
|
208
219
|
return { ...res, warning };
|
|
209
220
|
}
|
|
210
221
|
/** #3: the line clokio_stop_timer adds when the stopped entry lasted under a minute, else null. */
|
|
222
|
+
/**
|
|
223
|
+
* clokio_start_timer's answer: the entry line, and when force: true stopped a
|
|
224
|
+
* running timer (the server's `stopped_entry`, backend 2026-10-21), a
|
|
225
|
+
* "Stopped first" line naming it and, for one under a minute, the same
|
|
226
|
+
* warning clokio_stop_timer gives (MCP QA 0.28.0). An older server sends no
|
|
227
|
+
* stopped_entry and the answer is as before.
|
|
228
|
+
*/
|
|
229
|
+
export function withStoppedFirst(res) {
|
|
230
|
+
const out = withEntryLine(res);
|
|
231
|
+
const stopped = res?.stopped_entry;
|
|
232
|
+
if (!stopped || typeof stopped !== 'object' || out === null || typeof out !== 'object' || Array.isArray(out))
|
|
233
|
+
return out;
|
|
234
|
+
const warning = shortEntryWarning(stopped);
|
|
235
|
+
return { ...out, stopped_first: `Stopped first: ${timeEntryLine(stopped)}`, ...(warning ? { warning } : {}) };
|
|
236
|
+
}
|
|
211
237
|
export function shortEntryWarning(entry) {
|
|
212
238
|
const a = Date.parse(entry?.check_in ?? '');
|
|
213
239
|
const b = Date.parse(entry?.check_out ?? '');
|
|
@@ -405,19 +431,26 @@ export function registerAttendanceTools(server, config) {
|
|
|
405
431
|
'Needs the time_entries:write scope: Full access includes it; a key with chosen permissions must list it.';
|
|
406
432
|
const taskRule = 'The task must be one that person can view in your organisation, on an active project; a board another organisation ' +
|
|
407
433
|
'shared with you is refused (404).';
|
|
434
|
+
const targetRule = 'WHAT THE TIME IS ON, like the web Timer page (both optional): task_id, project_id, both, or neither. ' +
|
|
435
|
+
'Neither = a personal entry with no project (not billable). project_id alone = on that project with no task. ' +
|
|
436
|
+
"Both = the task decides and project_id must be the task's own project (refused otherwise). " +
|
|
437
|
+
"A project must be an active project of your organisation (clokio_list_projects). Entries hold no client: to log time for a client, use one of the client's projects. " +
|
|
438
|
+
taskRule;
|
|
408
439
|
registerTool(server, config, {
|
|
409
440
|
name: 'clokio_log_time',
|
|
410
|
-
description: 'Log a finished time entry on a task. ' + actingPerson + ' ' +
|
|
441
|
+
description: 'Log a finished time entry, on a task, a project, or neither. ' + actingPerson + ' ' +
|
|
411
442
|
'Times are local to that person (their location timezone, else UTC). Give EITHER date + start + end ' +
|
|
412
443
|
'(an end at or before start runs past midnight), OR date + start + duration_minutes, OR duration_minutes alone ' +
|
|
413
444
|
'(the entry then ENDS NOW and started that many minutes ago; date must be today or omitted). ' +
|
|
414
445
|
'At most 12 hours (720 minutes) per entry; nothing may start or end more than 10 minutes in the future; ' +
|
|
415
446
|
'an entry overlapping another time entry of the same person (including their running timer, and an overnight entry from the day before) is refused. ' +
|
|
416
|
-
'A duration counts real minutes across a DST change. ' +
|
|
417
|
-
"The project's billable flag is inherited and the description defaults to the task title. " +
|
|
447
|
+
'A duration counts real minutes across a DST change. ' + targetRule + ' ' +
|
|
448
|
+
"The project's billable flag is inherited (no project: not billable) and the description defaults to the task title (empty without a task). " +
|
|
418
449
|
ENTRY_LINE_NOTE,
|
|
419
450
|
schema: {
|
|
420
|
-
task_id: z.number().int().positive(),
|
|
451
|
+
task_id: z.number().int().positive().optional().describe('Optional. The task the time is on'),
|
|
452
|
+
project_id: z.number().int().positive().optional()
|
|
453
|
+
.describe("Optional. A project of your organisation; with task_id it must be the task's project"),
|
|
421
454
|
date: calendarDate('date').optional()
|
|
422
455
|
.describe('YYYY-MM-DD, the day the entry STARTS. Required with start'),
|
|
423
456
|
start: z.string().regex(/^\d{2}:\d{2}$/, 'start must be HH:MM').optional().describe('HH:MM, local time'),
|
|
@@ -441,6 +474,7 @@ export function registerAttendanceTools(server, config) {
|
|
|
441
474
|
method: 'POST',
|
|
442
475
|
body: {
|
|
443
476
|
task_id: args.task_id,
|
|
477
|
+
project_id: args.project_id,
|
|
444
478
|
date: args.date,
|
|
445
479
|
start: args.start,
|
|
446
480
|
end: args.end,
|
|
@@ -452,21 +486,26 @@ export function registerAttendanceTools(server, config) {
|
|
|
452
486
|
});
|
|
453
487
|
registerTool(server, config, {
|
|
454
488
|
name: 'clokio_start_timer',
|
|
455
|
-
description:
|
|
489
|
+
description: 'Start a running timer on a task, a project, or neither (a personal timer). ' + actingPerson + ' ' + targetRule + ' ' +
|
|
456
490
|
'If a timer is already running the call is refused (409) and the running entry is returned; ' +
|
|
457
|
-
'pass force: true to stop that timer and start this one
|
|
491
|
+
'pass force: true to stop that timer and start this one (the answer then names the stopped entry under stopped_first, ' +
|
|
492
|
+
'with a warning when it lasted under a minute). The description defaults to the task title (empty without a task). ' +
|
|
493
|
+
'clokio_stop_timer stops any timer; the web Timer page asks for a project and a description before its own Stop, ' +
|
|
494
|
+
'so give a personal timer a description (and stop it here) if the person may stop it on the web. ' +
|
|
458
495
|
ENTRY_LINE_NOTE,
|
|
459
496
|
schema: {
|
|
460
|
-
task_id: z.number().int().positive(),
|
|
497
|
+
task_id: z.number().int().positive().optional().describe('Optional. The task the timer is on'),
|
|
498
|
+
project_id: z.number().int().positive().optional()
|
|
499
|
+
.describe("Optional. A project of your organisation; with task_id it must be the task's project"),
|
|
461
500
|
description: z.string().max(255).optional(),
|
|
462
501
|
force: z.boolean().optional().describe('Stop a running timer first instead of being refused'),
|
|
463
502
|
},
|
|
464
503
|
mutates: true,
|
|
465
504
|
handler: async (args, cfg) => {
|
|
466
505
|
try {
|
|
467
|
-
return
|
|
506
|
+
return withStoppedFirst(await request(cfg, '/timer/start', {
|
|
468
507
|
method: 'POST',
|
|
469
|
-
body: { task_id: args.task_id, description: args.description, force: args.force },
|
|
508
|
+
body: { task_id: args.task_id, project_id: args.project_id, description: args.description, force: args.force },
|
|
470
509
|
}));
|
|
471
510
|
}
|
|
472
511
|
catch (e) {
|
|
@@ -502,7 +541,10 @@ export function registerAttendanceTools(server, config) {
|
|
|
502
541
|
registerTool(server, config, {
|
|
503
542
|
name: 'clokio_update_time_entry',
|
|
504
543
|
description: "Correct one of the time entries of the API key's ACTING person (another person's entry is \"Not found\"). " +
|
|
505
|
-
"Send only what changes: task_id (the entry moves to that task's project
|
|
544
|
+
"Send only what changes: task_id (the entry moves to that task's project; null unlinks the task and keeps the project), " +
|
|
545
|
+
'project_id (an active project of your organisation, or null to clear it; billable follows the project). ' +
|
|
546
|
+
"On an entry that is on a task, project_id must be the task's own project: to move it elsewhere send task_id: null with it. " +
|
|
547
|
+
'description (null or empty resets it to the task title, empty without a task), ' +
|
|
506
548
|
'and/or the time: date (YYYY-MM-DD), start, end (HH:MM, LOCAL to that person) or duration_minutes (alone it keeps the start and sets the length; start alone moves the entry and keeps its length). ' +
|
|
507
549
|
'Parts you leave out keep their current values. The same checks as clokio_log_time: at most 12 hours, nothing in the future, ' +
|
|
508
550
|
'no overlap with another entry of that person (their running timer included); an entry on an inactive project cannot change its time. ' +
|
|
@@ -510,7 +552,8 @@ export function registerAttendanceTools(server, config) {
|
|
|
510
552
|
'Needs time_entries:write.',
|
|
511
553
|
schema: {
|
|
512
554
|
id: z.number().int().positive().describe('The entry id (clokio_list_time_entries)'),
|
|
513
|
-
task_id: z.number().int().positive().optional(),
|
|
555
|
+
task_id: z.number().int().positive().nullable().optional().describe('A task to link, or null to unlink the task'),
|
|
556
|
+
project_id: z.number().int().positive().nullable().optional().describe('A project of your organisation, or null to clear it'),
|
|
514
557
|
date: calendarDate('date').optional(),
|
|
515
558
|
start: z.string().regex(/^\d{2}:\d{2}$/, 'start must be HH:MM').optional().describe('HH:MM, local time'),
|
|
516
559
|
end: z.string().regex(/^\d{2}:\d{2}$/, 'end must be HH:MM').optional().describe('HH:MM, local time. Not with duration_minutes'),
|
|
@@ -539,8 +582,7 @@ export function registerAttendanceTools(server, config) {
|
|
|
539
582
|
handler: async (args, cfg) => {
|
|
540
583
|
const res = await request(cfg, `/time-entries/${seg(args.id)}`, { method: 'DELETE' });
|
|
541
584
|
const e = res?.entry ?? {};
|
|
542
|
-
|
|
543
|
-
return `Deleted entry ${e.id ?? args.id}: ${e.date ?? '-'} ${entryClock(e)}, ${task}`;
|
|
585
|
+
return `Deleted entry ${e.id ?? args.id}: ${e.date ?? '-'} ${entryClock(e)}, ${entryTarget(e)}`;
|
|
544
586
|
},
|
|
545
587
|
});
|
|
546
588
|
registerTool(server, config, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clokio-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Clokio - manage tasks, projects, clients, employees, attendance and documents from Claude and any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|