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 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 97 tools to 31. Optional groups are `tasks`,
67
- `employees`, `attendance` (which includes leave and time entries), `docs` and
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
- 97 tools across seven areas. Read-only tools carry the MCP `readOnlyHint`; writes
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
@@ -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). ' +
@@ -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'),
@@ -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), labels (names) ' +
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.array(employeeCode).max(50).optional(),
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.array(employeeCode).min(1).max(50),
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.19.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": {