clokio-mcp 0.19.0 → 0.21.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 +32 -5
- package/dist/tools/attendance.js +83 -0
- package/dist/tools/employees.js +2 -0
- package/dist/tools/tasks.js +58 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,8 +63,8 @@ acting. An explicit code on a tool call always takes precedence.
|
|
|
63
63
|
|
|
64
64
|
Optional: set `CLOKIO_TOOLSETS` to register only the tool groups you need, e.g.
|
|
65
65
|
`CLOKIO_TOOLSETS=tasks`. This cuts the per-session tool-list cost - a
|
|
66
|
-
task-focused session drops from
|
|
67
|
-
`employees`, `attendance` (which includes leave and time
|
|
66
|
+
task-focused session drops from 98 tools to 32. Optional groups are `tasks`,
|
|
67
|
+
`employees`, `attendance` (which includes leave, time entries and logging time), `docs` and
|
|
68
68
|
`tickets`;
|
|
69
69
|
combine them with commas. If your key cannot open documents (no acts-as
|
|
70
70
|
employee and no explicit docs scopes), leaving `docs` out saves 33 tools it
|
|
@@ -74,16 +74,16 @@ through it.
|
|
|
74
74
|
|
|
75
75
|
## Tools
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
102 tools across seven areas. Read-only tools carry the MCP `readOnlyHint`; writes
|
|
78
78
|
are marked mutating (deletes are `destructive`).
|
|
79
79
|
|
|
80
80
|
| Area | Tools |
|
|
81
81
|
|------|-------|
|
|
82
|
-
| **Tasks** | list (with `compact` / `sort` / `updated_since` / `recurring`), get (with `comments_limit`), create, update (incl. recurring series: set a rule, pause/resume, stop), delete, bulk update, set assignees, list/add/edit comments, list attachments (with `is_latest` + `latest_only`), upload, download one (save to disk, or read PDF text / ZIP entries / images / text inline), download all to a folder, activity, list/add/remove dependencies, set custom field |
|
|
82
|
+
| **Tasks** | list (with `compact` / `sort` / `updated_since` / `recurring`), get (with `comments_limit`), create, update (incl. recurring series: set a rule, pause/resume, stop), delete, bulk update, set assignees (a shared board's client people as `{employee_code, organization_id}`), list a board's people, list/add/edit comments, list attachments (with `is_latest` + `latest_only`), upload, download one (save to disk, or read PDF text / ZIP entries / images / text inline), download all to a folder, activity, list/add/remove dependencies, set custom field |
|
|
83
83
|
| **Projects & clients** | list projects, project statuses, project custom fields, project contacts, get client, add client contact |
|
|
84
84
|
| **Reference** | task statuses, task labels, locations, list webhooks (read-only), whoami |
|
|
85
85
|
| **Employees** | list (name search), lookup by PIN, task-stats, inbox (mentions, new assignments, others' status changes), create, set status |
|
|
86
|
-
| **Attendance** | daily, range, per-employee, monthly summary, clock in/out, break start/end, time entries, leaves, leave balances |
|
|
86
|
+
| **Attendance** | daily, range, per-employee, monthly summary, clock in/out, break start/end, time entries, leaves, leave balances, **log time on a task**, start / stop / read the timer (see [Logging time](#logging-time)) |
|
|
87
87
|
| **Documents** (see [Documents](#documents) for who a key acts as) | list, tree, search, get (Markdown / HTML / JSON), create, replace content, update meta, move, archive, restore, delete, templates + create from template, members (by `employee_code`) and teams (list/add/update/remove, shareable teams), public share link, comments (list/add/resolve/reopen), versions (list/get/restore), attachments (list/upload/download) |
|
|
88
88
|
| **Tickets** (see [Tickets](#tickets)) | list (filters + cursor), get (with its newest messages; internal notes labelled, everything the customer wrote wrapped as untrusted), more messages, **reply to the customer** (fresh `Idempotency-Key` per call, `destructive`), internal note (with mentions), update state / assignee, add / remove one label, contacts list / get / update, inboxes, ticket labels, saved replies |
|
|
89
89
|
|
|
@@ -92,6 +92,11 @@ are marked mutating (deletes are `destructive`).
|
|
|
92
92
|
- **People are identified by `employee_code`** (e.g. `00080`), not internal ids.
|
|
93
93
|
Use `clokio_list_employees` (with `search`) to resolve a name to a code, or
|
|
94
94
|
`clokio_lookup_employee_by_pin` for a kiosk PIN.
|
|
95
|
+
- **Shared boards.** On a project shared with a client organisation, its
|
|
96
|
+
people appear on your tasks with `external: true` and `organization {id,
|
|
97
|
+
name}`. List them with `clokio_list_project_people` and assign them as
|
|
98
|
+
`{employee_code, organization_id}`: an employee code is unique only within
|
|
99
|
+
one organisation, so a bare code always means yours.
|
|
95
100
|
- **Attribution is automatic.** An unnamed write is credited to the key's
|
|
96
101
|
issuer (see `clokio_whoami`), so you rarely set an author. Pass
|
|
97
102
|
`created_by_employee_code` / `author_employee_code` only to credit someone
|
|
@@ -112,6 +117,28 @@ are marked mutating (deletes are `destructive`).
|
|
|
112
117
|
returns every occurrence of one series.
|
|
113
118
|
- An API key sees **public custom fields only**.
|
|
114
119
|
|
|
120
|
+
## Logging time
|
|
121
|
+
|
|
122
|
+
`clokio_log_time`, `clokio_start_timer`, `clokio_stop_timer` and
|
|
123
|
+
`clokio_get_active_timer` (0.21.0) call `/api/v1/time-entries` and
|
|
124
|
+
`/api/v1/timer/*`. The time is **always the key's acting person's** (its
|
|
125
|
+
"acts as" employee, else the person who issued it); no tool takes an
|
|
126
|
+
employee and the server refuses one.
|
|
127
|
+
|
|
128
|
+
- **Scopes:** `time_entries:write` for the three writes, `time_entries:read`
|
|
129
|
+
for the running timer. `time_entries:write` must be ticked on the key
|
|
130
|
+
explicitly: full access (`*`) never grants it.
|
|
131
|
+
- `clokio_log_time` takes `task_id` and EITHER `date` + `start` + `end`,
|
|
132
|
+
`date` + `start` + `duration_minutes`, or `duration_minutes` alone (the
|
|
133
|
+
entry then ends now). Times are local to that person. At most 12 hours per
|
|
134
|
+
entry; nothing may start or end more than 10 minutes in the future; an
|
|
135
|
+
overlap with another time entry of the same person (their running timer and
|
|
136
|
+
an overnight entry from the day before included) is refused.
|
|
137
|
+
- `clokio_start_timer` is refused while a timer runs; `force: true` stops it
|
|
138
|
+
and starts the new one. `clokio_stop_timer` is refused when nothing runs.
|
|
139
|
+
- The task must be visible to that person, on an active project of your
|
|
140
|
+
organisation; a board another organisation shared with you is refused.
|
|
141
|
+
|
|
115
142
|
## Documents
|
|
116
143
|
|
|
117
144
|
The Docs tools call `/api/v1/docs...` with the same `CLOKIO_API_KEY` as
|
package/dist/tools/attendance.js
CHANGED
|
@@ -183,6 +183,89 @@ export function registerAttendanceTools(server, config) {
|
|
|
183
183
|
},
|
|
184
184
|
}),
|
|
185
185
|
});
|
|
186
|
+
// Logging time (backend task #5775). The time is ALWAYS the key's acting
|
|
187
|
+
// person's: none of these tools takes an employee, and the server refuses
|
|
188
|
+
// one in the body. Scope time_entries:write is explicit-only ("*" never
|
|
189
|
+
// grants it).
|
|
190
|
+
const actingPerson = "The time is ALWAYS recorded for the API key's ACTING person (its \"acts as\" employee, else the person who issued it), " +
|
|
191
|
+
'never for anyone else; there is no way to log time for another employee. ' +
|
|
192
|
+
'Needs the time_entries:write scope, ticked explicitly on the key: full access ("*") does NOT grant it.';
|
|
193
|
+
const taskRule = 'The task must be one that person can view in your organisation, on an active project; a board another organisation ' +
|
|
194
|
+
'shared with you is refused (404).';
|
|
195
|
+
registerTool(server, config, {
|
|
196
|
+
name: 'clokio_log_time',
|
|
197
|
+
description: 'Log a finished time entry on a task. ' + actingPerson + ' ' +
|
|
198
|
+
'Times are local to that person (their location timezone, else UTC). Give EITHER date + start + end ' +
|
|
199
|
+
'(an end at or before start runs past midnight), OR date + start + duration_minutes, OR duration_minutes alone ' +
|
|
200
|
+
'(the entry then ENDS NOW and started that many minutes ago; date must be today or omitted). ' +
|
|
201
|
+
'At most 12 hours (720 minutes) per entry; nothing may start or end more than 10 minutes in the future; ' +
|
|
202
|
+
'an entry overlapping another time entry of the same person (including their running timer, and an overnight entry from the day before) is refused. ' +
|
|
203
|
+
'A duration counts real minutes across a DST change. ' + taskRule + ' ' +
|
|
204
|
+
"The project's billable flag is inherited and the description defaults to the task title.",
|
|
205
|
+
schema: {
|
|
206
|
+
task_id: z.number().int().positive(),
|
|
207
|
+
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'date must be YYYY-MM-DD').optional()
|
|
208
|
+
.describe('YYYY-MM-DD, the day the entry STARTS. Required with start'),
|
|
209
|
+
start: z.string().regex(/^\d{2}:\d{2}$/, 'start must be HH:MM').optional().describe('HH:MM, local time'),
|
|
210
|
+
end: z.string().regex(/^\d{2}:\d{2}$/, 'end must be HH:MM').optional()
|
|
211
|
+
.describe('HH:MM, local time. Give end OR duration_minutes, not both'),
|
|
212
|
+
duration_minutes: z.number().int().min(1).max(720).optional(),
|
|
213
|
+
description: z.string().max(255).optional(),
|
|
214
|
+
},
|
|
215
|
+
mutates: true,
|
|
216
|
+
handler: (args, cfg) => {
|
|
217
|
+
if ((args.end === undefined) === (args.duration_minutes === undefined)) {
|
|
218
|
+
throw new Error('Give exactly one of end or duration_minutes.');
|
|
219
|
+
}
|
|
220
|
+
if (args.start !== undefined && args.date === undefined) {
|
|
221
|
+
throw new Error('date is required with start.');
|
|
222
|
+
}
|
|
223
|
+
if (args.end !== undefined && args.start === undefined) {
|
|
224
|
+
throw new Error('start is required with end.');
|
|
225
|
+
}
|
|
226
|
+
return request(cfg, '/time-entries', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
body: {
|
|
229
|
+
task_id: args.task_id,
|
|
230
|
+
date: args.date,
|
|
231
|
+
start: args.start,
|
|
232
|
+
end: args.end,
|
|
233
|
+
duration_minutes: args.duration_minutes,
|
|
234
|
+
description: args.description,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
registerTool(server, config, {
|
|
240
|
+
name: 'clokio_start_timer',
|
|
241
|
+
description: "Start a running timer on a task. " + actingPerson + ' ' + taskRule + ' ' +
|
|
242
|
+
'If a timer is already running the call is refused (409) and the running entry is returned; ' +
|
|
243
|
+
'pass force: true to stop that timer and start this one. The description defaults to the task title.',
|
|
244
|
+
schema: {
|
|
245
|
+
task_id: z.number().int().positive(),
|
|
246
|
+
description: z.string().max(255).optional(),
|
|
247
|
+
force: z.boolean().optional().describe('Stop a running timer first instead of being refused'),
|
|
248
|
+
},
|
|
249
|
+
mutates: true,
|
|
250
|
+
handler: (args, cfg) => request(cfg, '/timer/start', {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
body: { task_id: args.task_id, description: args.description, force: args.force },
|
|
253
|
+
}),
|
|
254
|
+
});
|
|
255
|
+
registerTool(server, config, {
|
|
256
|
+
name: 'clokio_stop_timer',
|
|
257
|
+
description: "Stop the running timer of the API key's ACTING person (never anyone else's) and return the entry with its hours. " +
|
|
258
|
+
'Refused (409) when nothing is running. Needs time_entries:write (not granted by "*").',
|
|
259
|
+
schema: {},
|
|
260
|
+
mutates: true,
|
|
261
|
+
handler: (_args, cfg) => request(cfg, '/timer/stop', { method: 'POST' }),
|
|
262
|
+
});
|
|
263
|
+
registerTool(server, config, {
|
|
264
|
+
name: 'clokio_get_active_timer',
|
|
265
|
+
description: "The running timer of the API key's ACTING person, or entry: null when none runs. Needs time_entries:read.",
|
|
266
|
+
schema: {},
|
|
267
|
+
handler: (_args, cfg) => request(cfg, '/timer/active'),
|
|
268
|
+
});
|
|
186
269
|
registerTool(server, config, {
|
|
187
270
|
name: 'clokio_list_leaves',
|
|
188
271
|
description: 'Leave requests in a date range. from and to are both REQUIRED (YYYY-MM-DD, to >= from). ' +
|
package/dist/tools/employees.js
CHANGED
|
@@ -18,6 +18,8 @@ export function registerEmployeeTools(server, config) {
|
|
|
18
18
|
'wants for assignees, comment authors and task creators. Pass `search` with a name (or part of one) ' +
|
|
19
19
|
'rather than listing everyone and matching yourself. Combine with status=active to skip people who have ' +
|
|
20
20
|
'left. email is returned only if the key holds the employees:pii scope. ' +
|
|
21
|
+
"It lists the key's OWN organisation only: people of a client organisation on a shared board are not " +
|
|
22
|
+
'here - get them, with their organization.id, from clokio_list_project_people. ' +
|
|
21
23
|
'Paginated: the response is {data, meta}, the people under data.',
|
|
22
24
|
schema: {
|
|
23
25
|
search: z.string().max(120).optional().describe('Free text over name and employee_code'),
|
package/dist/tools/tasks.js
CHANGED
|
@@ -7,6 +7,29 @@ import { asAttachmentArray, downloadAttachment, safeAttachmentName, stampIsLates
|
|
|
7
7
|
const employeeCode = z
|
|
8
8
|
.string()
|
|
9
9
|
.regex(/^[A-Za-z0-9_-]{1,50}$/, 'employee_code must be 1-50 chars of letters, digits, _ or -');
|
|
10
|
+
/**
|
|
11
|
+
* One assignee (Clokio Public API, spec 2026-10-16). A STRING is an
|
|
12
|
+
* employee_code of the key's OWN organisation. On a board shared with a
|
|
13
|
+
* client organisation, a person of THAT organisation is
|
|
14
|
+
* {employee_code, organization_id}: codes are unique per organisation only,
|
|
15
|
+
* so a bare code can never name them and may collide with one of yours.
|
|
16
|
+
* organization_id is the numeric organization.id clokio_list_project_people
|
|
17
|
+
* reports, never an organisation login code. .strict(): an extra key is a
|
|
18
|
+
* mistake the server would refuse, so it is refused here first.
|
|
19
|
+
*/
|
|
20
|
+
export const assigneeReference = z.union([
|
|
21
|
+
employeeCode,
|
|
22
|
+
z
|
|
23
|
+
.object({
|
|
24
|
+
employee_code: employeeCode,
|
|
25
|
+
organization_id: z.number().int().positive(),
|
|
26
|
+
})
|
|
27
|
+
.strict(),
|
|
28
|
+
]);
|
|
29
|
+
const SHARED_BOARD_NOTE = 'On a project SHARED with a client organisation, call clokio_list_project_people first: a person whose ' +
|
|
30
|
+
'`external` is true is assigned as {employee_code, organization_id} (organization.id from that tool), never ' +
|
|
31
|
+
'by a bare code - a bare code always means YOUR organisation and can collide with theirs. Bare string codes ' +
|
|
32
|
+
'for your own staff work exactly as before, and the two forms can be mixed in one list.';
|
|
10
33
|
/**
|
|
11
34
|
* Recurrence, as a structured object the agent fills, serialized here into
|
|
12
35
|
* the rule string the Clokio scheduler executes (`daily`, `weekly:sun,wed`,
|
|
@@ -176,7 +199,9 @@ export function registerTaskTools(server, config) {
|
|
|
176
199
|
.boolean()
|
|
177
200
|
.optional()
|
|
178
201
|
.describe('STRONGLY PREFERRED when scanning. Returns a slim row - id, title, status, priority, ' +
|
|
179
|
-
'due_date, parent_id, project_id, assignee_codes (employee_code strings
|
|
202
|
+
'due_date, parent_id, project_id, assignee_codes (employee_code strings of YOUR organisation), ' +
|
|
203
|
+
'external_assignees ([{employee_code, organization_id}] for a client organisation\'s people on a ' +
|
|
204
|
+
'shared board, [] when none; organization_id null when that organisation is hidden), labels (names) ' +
|
|
180
205
|
'and updated_at - dropping the HTML description and flattening the nested project/assignee ' +
|
|
181
206
|
'objects to ids. Resolve a project_id with clokio_list_projects, and fetch the one task you ' +
|
|
182
207
|
'actually need in full with clokio_get_task (its URL is /tasks/{id}/view).'),
|
|
@@ -227,7 +252,10 @@ export function registerTaskTools(server, config) {
|
|
|
227
252
|
name: 'clokio_get_task',
|
|
228
253
|
description: 'Get one task by id, with its full detail (assignees, labels, status, custom fields, task_url) and ' +
|
|
229
254
|
'its comments. A long-running task can carry dozens of comments: pass comments_limit to keep only the ' +
|
|
230
|
-
'newest few, or 0 for none. comments_count always reports the real total.'
|
|
255
|
+
'newest few, or 0 for none. comments_count always reports the real total. Every person in it ' +
|
|
256
|
+
'(created_by, assignees, comment authors, uploaders) carries organization {id, name} and external: ' +
|
|
257
|
+
'an employee_code is unique only within one organisation, so pair it with organization.id when ' +
|
|
258
|
+
'external is true (organization is null when that organisation is hidden from you).',
|
|
231
259
|
schema: {
|
|
232
260
|
id: z.number().int(),
|
|
233
261
|
comments_limit: z
|
|
@@ -248,7 +276,9 @@ export function registerTaskTools(server, config) {
|
|
|
248
276
|
'automatically (whoami shows who that is). Set it only to credit someone OTHER than the key owner. ' +
|
|
249
277
|
'Pass parent_task_id to create it as a SUBTASK of that task ' +
|
|
250
278
|
'(same project, and the parent must not itself be a subtask - one level deep only).\n\n' +
|
|
251
|
-
RECURRENCE_MODEL_NOTE
|
|
279
|
+
RECURRENCE_MODEL_NOTE +
|
|
280
|
+
'\n\n' +
|
|
281
|
+
SHARED_BOARD_NOTE,
|
|
252
282
|
schema: {
|
|
253
283
|
project_id: z.number().int(),
|
|
254
284
|
title: z.string().max(255),
|
|
@@ -259,7 +289,11 @@ export function registerTaskTools(server, config) {
|
|
|
259
289
|
start_date: z.string().optional().describe('YYYY-MM-DD'),
|
|
260
290
|
estimated_hours: z.number().min(0).max(9999.99).optional(),
|
|
261
291
|
parent_task_id: z.number().int().optional().describe('Makes this a subtask of that task'),
|
|
262
|
-
assignee_employee_codes: z
|
|
292
|
+
assignee_employee_codes: z
|
|
293
|
+
.array(assigneeReference)
|
|
294
|
+
.max(50)
|
|
295
|
+
.optional()
|
|
296
|
+
.describe('Each a code of your organisation, or {employee_code, organization_id} for a client\'s person on a shared board'),
|
|
263
297
|
label_names: z
|
|
264
298
|
.array(z.string().max(100))
|
|
265
299
|
.max(20)
|
|
@@ -401,10 +435,16 @@ export function registerTaskTools(server, config) {
|
|
|
401
435
|
name: 'clokio_set_task_assignees',
|
|
402
436
|
mutates: true,
|
|
403
437
|
description: "Change a task's assignees. mode is REQUIRED: \"add\" appends, \"replace\" overwrites the whole list, " +
|
|
404
|
-
'"remove" takes those people off. People are named by employee_code (e.g. "00080").'
|
|
438
|
+
'"remove" takes those people off. People are named by employee_code (e.g. "00080"). Adding someone ' +
|
|
439
|
+
'already assigned leaves the assignee list and the activity log unchanged. Assigning through the API sends the assignee no notification.\n\n' +
|
|
440
|
+
SHARED_BOARD_NOTE,
|
|
405
441
|
schema: {
|
|
406
442
|
id: z.number().int(),
|
|
407
|
-
employee_codes: z
|
|
443
|
+
employee_codes: z
|
|
444
|
+
.array(assigneeReference)
|
|
445
|
+
.min(1)
|
|
446
|
+
.max(50)
|
|
447
|
+
.describe('Each a code of your organisation, or {employee_code, organization_id} for a client\'s person on a shared board'),
|
|
408
448
|
mode: z.enum(['add', 'replace', 'remove']).describe('Required'),
|
|
409
449
|
},
|
|
410
450
|
handler: (args, cfg) => request(cfg, `/tasks/${args.id}/assignees`, {
|
|
@@ -412,6 +452,18 @@ export function registerTaskTools(server, config) {
|
|
|
412
452
|
body: { employee_codes: args.employee_codes, mode: args.mode },
|
|
413
453
|
}),
|
|
414
454
|
});
|
|
455
|
+
registerTool(server, config, {
|
|
456
|
+
name: 'clokio_list_project_people',
|
|
457
|
+
description: "Who is on a project's board: its members and, on a board shared with a client organisation, the client's " +
|
|
458
|
+
'nominated people and admins (active people only; coding agents are left out). Each row: employee_code, ' +
|
|
459
|
+
'name, organization {id, name}, external. Your organisation comes first. Call this BEFORE assigning on a ' +
|
|
460
|
+
'shared board: an external person is assigned as {employee_code, organization_id} (organization.id from ' +
|
|
461
|
+
'here). Your own staff who are not board members are still assignable by bare employee_code ' +
|
|
462
|
+
'(clokio_list_employees). email appears on your own rows only, and only with the employees:pii scope. ' +
|
|
463
|
+
'Needs the employees:read scope.',
|
|
464
|
+
schema: { project_id: z.number().int() },
|
|
465
|
+
handler: (args, cfg) => request(cfg, `/projects/${args.project_id}/people`),
|
|
466
|
+
});
|
|
415
467
|
registerTool(server, config, {
|
|
416
468
|
name: 'clokio_list_task_comments',
|
|
417
469
|
description: 'List the comments on a task.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clokio-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
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": {
|