clokio-mcp 0.26.0 → 0.27.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 +4 -4
- package/dist/tools/projects.js +155 -0
- package/dist/tools/tasks.js +243 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ acting. An explicit code on a tool call always takes precedence.
|
|
|
65
65
|
|
|
66
66
|
Optional: set `CLOKIO_TOOLSETS` to register only the tool groups you need, e.g.
|
|
67
67
|
`CLOKIO_TOOLSETS=tasks`. This cuts the per-session tool-list cost - a
|
|
68
|
-
task-focused session drops from
|
|
68
|
+
task-focused session drops from 127 tools to 40. Optional groups are `tasks`,
|
|
69
69
|
`employees`, `attendance` (which includes leave, time entries and logging time), `docs` and
|
|
70
70
|
`tickets`;
|
|
71
71
|
combine them with commas. If your key cannot open documents (no acts-as
|
|
@@ -76,13 +76,13 @@ through it.
|
|
|
76
76
|
|
|
77
77
|
## Tools
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
127 tools across seven areas. Read-only tools carry the MCP `readOnlyHint`; writes
|
|
80
80
|
are marked mutating (deletes are `destructive`).
|
|
81
81
|
|
|
82
82
|
| Area | Tools |
|
|
83
83
|
|------|-------|
|
|
84
|
-
| **Tasks** | list (with `compact` / `sort` / `updated_since` / `recurring`), get (with `comments_limit`; names the ticket a task was opened from), create, update (incl. recurring series: set a rule, pause/resume, stop; labels: replace, add, remove), delete, bulk update (status, priority, due date, add / remove labels), 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, read images pasted into descriptions and comments, activity (with organisations, paged), list/add/remove dependencies, set custom field (an option by value or label), set custom fields on many tasks (`clokio_bulk_set_task_custom_field`, partial success); `clokio_update_task` takes `custom_fields` too |
|
|
85
|
-
| **Projects & clients** | list projects, project statuses, project custom fields, project contacts, get client, add client contact |
|
|
84
|
+
| **Tasks** | list (with `compact` / `sort` / `updated_since` / `updated_before` / `recurring`, no due date, no assignee, a custom field value, title-only search), a project's task summary (counts by status / priority / label / assignee, overdue and stale lists, per option of a release dropdown: `clokio_get_project_task_summary`), a project's activity log (`clokio_list_project_activity`), get (with `comments_limit`; names the ticket a task was opened from), create, update (incl. recurring series: set a rule, pause/resume, stop; labels: replace, add, remove; parent task; assignees in the same call), delete, bulk update (status, priority, due date, estimate, add / remove labels), 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, read images pasted into descriptions and comments, activity (with organisations, paged), list/add/remove dependencies, set custom field (an option by value or label), set custom fields on many tasks (`clokio_bulk_set_task_custom_field`, partial success); `clokio_update_task` takes `custom_fields` too |
|
|
85
|
+
| **Projects & clients** | list projects, project statuses (and create / edit them), project custom fields (and create / edit them, e.g. a release dropdown), project contacts, get client, add client contact |
|
|
86
86
|
| **Reference** | task statuses, task labels, locations, list webhooks (read-only), whoami |
|
|
87
87
|
| **Employees** | list (name search), lookup by PIN, task-stats, inbox (mentions, new assignments, others' status changes; with the tickets toolset, also the acting employee's open tickets), create, set status |
|
|
88
88
|
| **Attendance** | daily, range, per-employee, monthly summary, clock in/out, break start/end, time entries (local times in the person's zone; edit / delete your own), leaves (with `days`, half days counted 0.5), days of leave taken per type and year (`clokio_get_leave_days_taken`; the old name `clokio_get_leave_balances` still works; Clokio tracks no quota or remaining balance; leave and time-entry lists take `employee_code` and at most 92 days per call), **log time on a task**, start / stop / read the timer (see [Logging time](#logging-time)) |
|
package/dist/tools/projects.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { request } from '../client.js';
|
|
3
3
|
import { registerTool } from './helpers.js';
|
|
4
4
|
import { VERSION } from '../version.js';
|
|
5
|
+
import { FIELD_KEY_PATTERN } from './tasks.js';
|
|
5
6
|
/** Projects, clients, and the board-configuration + reference lookups. */
|
|
6
7
|
export function registerProjectTools(server, config) {
|
|
7
8
|
registerTool(server, config, {
|
|
@@ -29,6 +30,160 @@ export function registerProjectTools(server, config) {
|
|
|
29
30
|
schema: { project_id: z.number().int() },
|
|
30
31
|
handler: (args, cfg) => request(cfg, `/projects/${args.project_id}/custom-fields`),
|
|
31
32
|
});
|
|
33
|
+
// ── Board configuration writes (0.27.0, backend #5874 part 4) ──
|
|
34
|
+
// projects:write. Every rule is the web editor's own copy on the server;
|
|
35
|
+
// nothing is judged here beyond the shapes. No delete in this version.
|
|
36
|
+
const fieldOptions = z
|
|
37
|
+
.array(z.object({
|
|
38
|
+
value: z.string().min(1).max(100).describe('The stored value tasks hold; keep it stable'),
|
|
39
|
+
label: z.string().max(100).optional().describe('What people see; defaults to the value'),
|
|
40
|
+
color: z
|
|
41
|
+
.string()
|
|
42
|
+
.regex(/^#[0-9a-fA-F]{6}$/, 'color must be #rrggbb')
|
|
43
|
+
.optional(),
|
|
44
|
+
}))
|
|
45
|
+
.min(1)
|
|
46
|
+
.max(200);
|
|
47
|
+
const fieldType = z.enum(['text', 'number', 'date', 'dropdown', 'checkbox', 'multi_select', 'url', 'email']);
|
|
48
|
+
const actorCode = z
|
|
49
|
+
.string()
|
|
50
|
+
.regex(/^[A-Za-z0-9_-]{1,50}$/, 'employee_code: letters, digits, _ or -')
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Checked by the server (an unknown code is refused); a configuration edit writes no activity row');
|
|
53
|
+
const statusColor = z
|
|
54
|
+
.string()
|
|
55
|
+
.regex(/^#[0-9a-fA-F]{6}$/, 'color must be #rrggbb')
|
|
56
|
+
.nullable()
|
|
57
|
+
.optional();
|
|
58
|
+
registerTool(server, config, {
|
|
59
|
+
name: 'clokio_create_project_custom_field',
|
|
60
|
+
mutates: true,
|
|
61
|
+
description: 'Create a custom field on a project (the board\'s columns). type is one of text, number, date, dropdown, ' +
|
|
62
|
+
'checkbox, multi_select, url, email. A dropdown or multi_select NEEDS options ({value, label?, color?}; ' +
|
|
63
|
+
'the label defaults to the value); every other type takes none. The field key is generated from the ' +
|
|
64
|
+
'name and never changes (it is what clokio_set_task_custom_field and the list filters take). An API ' +
|
|
65
|
+
'key creates PUBLIC fields only. A release or sprint is a dropdown field: create it once, add an ' +
|
|
66
|
+
'option per release with clokio_update_project_custom_field, set it on tasks with ' +
|
|
67
|
+
'clokio_bulk_set_task_custom_field, read it with clokio_get_project_task_summary group_by_custom_field. ' +
|
|
68
|
+
'Needs the projects:write scope.',
|
|
69
|
+
schema: {
|
|
70
|
+
project_id: z.number().int(),
|
|
71
|
+
name: z.string().min(1).max(50),
|
|
72
|
+
type: fieldType,
|
|
73
|
+
options: fieldOptions.optional(),
|
|
74
|
+
is_required: z.boolean().optional(),
|
|
75
|
+
actor_employee_code: actorCode,
|
|
76
|
+
},
|
|
77
|
+
handler: (args, cfg) => {
|
|
78
|
+
const optioned = args.type === 'dropdown' || args.type === 'multi_select';
|
|
79
|
+
if (optioned && args.options === undefined) {
|
|
80
|
+
throw new Error(`A ${args.type} field needs options: give at least one {value, label}.`);
|
|
81
|
+
}
|
|
82
|
+
if (!optioned && args.options !== undefined) {
|
|
83
|
+
throw new Error(`A ${args.type} field takes no options; only dropdown and multi_select do.`);
|
|
84
|
+
}
|
|
85
|
+
const body = { name: args.name, type: args.type };
|
|
86
|
+
if (args.options !== undefined)
|
|
87
|
+
body.options = args.options;
|
|
88
|
+
if (args.is_required !== undefined)
|
|
89
|
+
body.is_required = args.is_required;
|
|
90
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
91
|
+
if (actor)
|
|
92
|
+
body.actor_employee_code = actor;
|
|
93
|
+
return request(cfg, `/projects/${args.project_id}/custom-fields`, { method: 'POST', body });
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
registerTool(server, config, {
|
|
97
|
+
name: 'clokio_update_project_custom_field',
|
|
98
|
+
mutates: true,
|
|
99
|
+
description: 'Edit a project custom field by its field_key: rename it (the key stays), change its type, flip ' +
|
|
100
|
+
'is_required, or replace its options with the WHOLE new list. To add a release, read the field with ' +
|
|
101
|
+
'clokio_get_project_custom_fields and send every option plus the new one; renaming a label or ' +
|
|
102
|
+
'reordering is free (tasks store the value). Removing an option tasks still hold is refused naming ' +
|
|
103
|
+
'it; so is a type change that would leave stored answers unreadable. A private field answers not ' +
|
|
104
|
+
'found. Needs the projects:write scope.',
|
|
105
|
+
schema: {
|
|
106
|
+
project_id: z.number().int(),
|
|
107
|
+
field_key: z.string().regex(FIELD_KEY_PATTERN, 'field_key: letters, digits, _ or -'),
|
|
108
|
+
name: z.string().min(1).max(50).optional(),
|
|
109
|
+
type: fieldType.optional(),
|
|
110
|
+
options: fieldOptions.optional(),
|
|
111
|
+
is_required: z.boolean().optional(),
|
|
112
|
+
actor_employee_code: actorCode,
|
|
113
|
+
},
|
|
114
|
+
handler: (args, cfg) => {
|
|
115
|
+
const body = {};
|
|
116
|
+
for (const key of ['name', 'type', 'options', 'is_required']) {
|
|
117
|
+
if (args[key] !== undefined)
|
|
118
|
+
body[key] = args[key];
|
|
119
|
+
}
|
|
120
|
+
if (Object.keys(body).length === 0) {
|
|
121
|
+
throw new Error('Nothing to change: give at least one of name, type, options, is_required.');
|
|
122
|
+
}
|
|
123
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
124
|
+
if (actor)
|
|
125
|
+
body.actor_employee_code = actor;
|
|
126
|
+
return request(cfg, `/projects/${args.project_id}/custom-fields/${args.field_key}`, { method: 'PATCH', body });
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
registerTool(server, config, {
|
|
130
|
+
name: 'clokio_create_project_status',
|
|
131
|
+
mutates: true,
|
|
132
|
+
description: 'Add a status (a board column) to a project. The name must be new in the project (compared ignoring ' +
|
|
133
|
+
'case); the slug is generated from it and never changes. A new status is always ACTIVE: a project has ' +
|
|
134
|
+
'exactly one completed status, which always stays the last column. position (0-based) places it. A ' +
|
|
135
|
+
'project still using the organisation\'s statuses gets its own copy first, so no other project ' +
|
|
136
|
+
'changes. Needs the projects:write scope.',
|
|
137
|
+
schema: {
|
|
138
|
+
project_id: z.number().int(),
|
|
139
|
+
name: z.string().min(1).max(50),
|
|
140
|
+
color: statusColor.describe('#rrggbb'),
|
|
141
|
+
position: z.number().int().min(0).max(200).optional().describe('0-based place on the board'),
|
|
142
|
+
actor_employee_code: actorCode,
|
|
143
|
+
},
|
|
144
|
+
handler: (args, cfg) => {
|
|
145
|
+
const body = { name: args.name };
|
|
146
|
+
if (args.color !== undefined)
|
|
147
|
+
body.color = args.color;
|
|
148
|
+
if (args.position !== undefined)
|
|
149
|
+
body.position = args.position;
|
|
150
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
151
|
+
if (actor)
|
|
152
|
+
body.actor_employee_code = actor;
|
|
153
|
+
return request(cfg, `/projects/${args.project_id}/statuses`, { method: 'POST', body });
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
registerTool(server, config, {
|
|
157
|
+
name: 'clokio_update_project_status',
|
|
158
|
+
mutates: true,
|
|
159
|
+
description: 'Edit a project status by its slug (clokio_get_project_statuses): rename (the slug stays), recolour, ' +
|
|
160
|
+
'move (position, 0-based; the completed status stays last) or change category. A status cannot become ' +
|
|
161
|
+
'a second completed one, and the completed one cannot become active. Statuses cannot be deleted ' +
|
|
162
|
+
'through the API. Needs the projects:write scope.',
|
|
163
|
+
schema: {
|
|
164
|
+
project_id: z.number().int(),
|
|
165
|
+
slug: z.string().regex(/^[A-Za-z0-9_-]{1,100}$/, 'slug: letters, digits, _ or -'),
|
|
166
|
+
name: z.string().min(1).max(50).optional(),
|
|
167
|
+
color: statusColor.describe('#rrggbb, or null for the default colour'),
|
|
168
|
+
category: z.enum(['active', 'done']).optional(),
|
|
169
|
+
position: z.number().int().min(0).max(200).optional().describe('0-based place on the board'),
|
|
170
|
+
actor_employee_code: actorCode,
|
|
171
|
+
},
|
|
172
|
+
handler: (args, cfg) => {
|
|
173
|
+
const body = {};
|
|
174
|
+
for (const key of ['name', 'color', 'category', 'position']) {
|
|
175
|
+
if (args[key] !== undefined)
|
|
176
|
+
body[key] = args[key];
|
|
177
|
+
}
|
|
178
|
+
if (Object.keys(body).length === 0) {
|
|
179
|
+
throw new Error('Nothing to change: give at least one of name, color, category, position.');
|
|
180
|
+
}
|
|
181
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
182
|
+
if (actor)
|
|
183
|
+
body.actor_employee_code = actor;
|
|
184
|
+
return request(cfg, `/projects/${args.project_id}/statuses/${args.slug}`, { method: 'PATCH', body });
|
|
185
|
+
},
|
|
186
|
+
});
|
|
32
187
|
registerTool(server, config, {
|
|
33
188
|
name: 'clokio_get_project_contacts',
|
|
34
189
|
description: "The contacts for a project's client.",
|
package/dist/tools/tasks.js
CHANGED
|
@@ -7,6 +7,16 @@ import { asAttachmentArray, downloadAttachment, downloadTaskMedia, inlineMediaRo
|
|
|
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
|
+
* A custom field's field_key, by the backend's own rule: the route
|
|
12
|
+
* `->where('fieldKey', '[A-Za-z0-9_-]{1,50}')` and the 50-char `key` column
|
|
13
|
+
* (keys are generated with Str::slug). Every field_key input uses it, so a
|
|
14
|
+
* key such as `a]b` or `x&y` never reaches a query string or a path.
|
|
15
|
+
*/
|
|
16
|
+
export const FIELD_KEY_PATTERN = /^[A-Za-z0-9_-]{1,50}$/;
|
|
17
|
+
const fieldKey = z.string().regex(FIELD_KEY_PATTERN, 'field_key must be 1-50 chars of letters, digits, _ or -');
|
|
18
|
+
/** THE estimated_hours ceiling of every Public API write (create, update, bulk): the backend's ESTIMATED_HOURS_MAX. */
|
|
19
|
+
export const ESTIMATED_HOURS_MAX = 9999.99;
|
|
10
20
|
/**
|
|
11
21
|
* One custom field value as the API takes it: text / number / date string,
|
|
12
22
|
* true / false for a checkbox, an option's value OR label for a dropdown, a
|
|
@@ -18,7 +28,7 @@ const customFieldValue = z
|
|
|
18
28
|
.describe('The value; a dropdown option by its value or its label; null clears');
|
|
19
29
|
/** field_key -> value, at least one and at most 50 fields (the server's cap). */
|
|
20
30
|
const customFieldsMap = z
|
|
21
|
-
.record(
|
|
31
|
+
.record(fieldKey, customFieldValue)
|
|
22
32
|
.refine((m) => Object.keys(m).length >= 1 && Object.keys(m).length <= 50, {
|
|
23
33
|
message: 'custom_fields must name 1-50 fields',
|
|
24
34
|
})
|
|
@@ -279,6 +289,32 @@ export function renderActivity(res) {
|
|
|
279
289
|
`Page ${meta.current_page ?? '-'} of ${meta.last_page ?? '-'} (total ${meta.total ?? '-'}); pass page to continue.`,
|
|
280
290
|
].join('\n');
|
|
281
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* 0.27.0 (backend #5874 part 4): a project's activity feed, one change per
|
|
294
|
+
* line like renderActivity, each naming its task, then the keyset cursor to
|
|
295
|
+
* continue with. Values are flattened the same way, so a value cannot print
|
|
296
|
+
* something that looks like further lines.
|
|
297
|
+
*/
|
|
298
|
+
export function renderProjectActivity(res) {
|
|
299
|
+
const rows = Array.isArray(res) ? res : (res?.data ?? []);
|
|
300
|
+
const meta = res?.meta ?? {};
|
|
301
|
+
const flat = (v) => String(v).replace(/\s+/g, ' ').trim();
|
|
302
|
+
const value = (v) => {
|
|
303
|
+
if (v === null || v === undefined)
|
|
304
|
+
return '-';
|
|
305
|
+
const f = flat(typeof v === 'object' ? JSON.stringify(v) : v);
|
|
306
|
+
return f === '' ? '-' : f;
|
|
307
|
+
};
|
|
308
|
+
const task = (t) => t ? `task #${flat(t.id)}${t.title ? ` "${flat(t.title)}"` : ''}${t.deleted ? ' (deleted)' : ''}` : 'task -';
|
|
309
|
+
const line = (a) => `[${flat(a.created_at)}] ${task(a.task)} - ${flat(activityActor(a.employee))}: ${flat(a.action)}${a.field ? ` ${flat(a.field)}` : ''}` +
|
|
310
|
+
(a.old_value != null || a.new_value != null ? ` (${value(a.old_value)} -> ${value(a.new_value)})` : '');
|
|
311
|
+
return [
|
|
312
|
+
...(rows.length ? rows.map(line) : ['No activity.']),
|
|
313
|
+
meta.next_cursor
|
|
314
|
+
? `More: pass cursor="${flat(meta.next_cursor)}" (with the same filters) for the next page.`
|
|
315
|
+
: 'End of the log for these filters.',
|
|
316
|
+
].join('\n');
|
|
317
|
+
}
|
|
282
318
|
/**
|
|
283
319
|
* #4 (0.23.0): the ticket a task was opened from, as `linked_ticket:
|
|
284
320
|
* "ticket #<id> <url>"` first in the answer, replacing the raw `ticket`
|
|
@@ -294,6 +330,39 @@ export function withLinkedTicket(task) {
|
|
|
294
330
|
}
|
|
295
331
|
return rest;
|
|
296
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* The list filters of backend task #5874 part 3, turned into GET /tasks query
|
|
335
|
+
* keys: `custom_field` (an object) becomes custom_field[<field_key>]=<value>,
|
|
336
|
+
* a boolean checkbox value 1 / 0. The contradictions the server refuses with a
|
|
337
|
+
* 422 are refused here first, in the same words, so no request is spent on
|
|
338
|
+
* them.
|
|
339
|
+
*/
|
|
340
|
+
export function listFilterQuery(args) {
|
|
341
|
+
if (args.no_due_date === true && (args.due_before !== undefined || args.due_after !== undefined)) {
|
|
342
|
+
throw new Error('no_due_date=true cannot be combined with due_before or due_after: a task with no due date is never due before or after anything. Drop one of them.');
|
|
343
|
+
}
|
|
344
|
+
if (args.no_assignee === true && args.assignee_employee_code !== undefined) {
|
|
345
|
+
throw new Error('no_assignee=true cannot be combined with assignee_employee_code: a task with no assignee is held by nobody. Drop one of them.');
|
|
346
|
+
}
|
|
347
|
+
const out = {
|
|
348
|
+
no_due_date: args.no_due_date === undefined ? undefined : args.no_due_date ? 1 : 0,
|
|
349
|
+
no_assignee: args.no_assignee === undefined ? undefined : args.no_assignee ? 1 : 0,
|
|
350
|
+
search_in: args.search_in,
|
|
351
|
+
updated_before: args.updated_before,
|
|
352
|
+
};
|
|
353
|
+
if (args.custom_field !== undefined) {
|
|
354
|
+
if (args.project_id === undefined) {
|
|
355
|
+
throw new Error('custom_field filters need project_id: custom fields belong to one project. Add project_id; clokio_get_project_custom_fields lists its field keys.');
|
|
356
|
+
}
|
|
357
|
+
for (const [key, value] of Object.entries(args.custom_field)) {
|
|
358
|
+
if (!FIELD_KEY_PATTERN.test(key)) {
|
|
359
|
+
throw new Error(`custom_field key "${key}" is not a field_key: 1-50 chars of letters, digits, _ or -.`);
|
|
360
|
+
}
|
|
361
|
+
out[`custom_field[${key}]`] = typeof value === 'boolean' ? (value ? 1 : 0) : value;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
297
366
|
export function registerTaskTools(server, config) {
|
|
298
367
|
registerTool(server, config, {
|
|
299
368
|
name: 'clokio_list_tasks',
|
|
@@ -301,14 +370,44 @@ export function registerTaskTools(server, config) {
|
|
|
301
370
|
'your own code. An UNKNOWN query parameter is a 422 naming it, so use exactly these names. Paginate ' +
|
|
302
371
|
'with page, or crawl with cursor (preferred past one page). Every task carries a task_url.\n\n' +
|
|
303
372
|
'The response is {data, meta}. meta carries total, per_page, current_page and next_cursor - read ' +
|
|
304
|
-
'next_cursor and pass it back as `cursor` for the following page; it is null on the last one.'
|
|
373
|
+
'next_cursor and pass it back as `cursor` for the following page; it is null on the last one. ' +
|
|
374
|
+
'For counts rather than rows (how many open tasks per status, label or person, what is overdue or ' +
|
|
375
|
+
'stale on one board), use clokio_get_project_task_summary instead of paging.',
|
|
305
376
|
schema: {
|
|
306
377
|
project_id: z.number().int().optional(),
|
|
307
378
|
status: z.string().optional().describe('Status slug, e.g. "in_progress"'),
|
|
308
379
|
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
|
|
309
380
|
assignee_employee_code: employeeCode.optional().describe('employee_code, e.g. "00080"'),
|
|
310
381
|
label: z.string().optional(),
|
|
311
|
-
search: z.string().optional().describe('Free text over title and description'),
|
|
382
|
+
search: z.string().optional().describe('Free text over title and description (see search_in)'),
|
|
383
|
+
search_in: z
|
|
384
|
+
.enum(['title', 'all'])
|
|
385
|
+
.optional()
|
|
386
|
+
.describe('Where `search` looks: "title" only, or "all" = title and description (the default)'),
|
|
387
|
+
no_due_date: z
|
|
388
|
+
.boolean()
|
|
389
|
+
.optional()
|
|
390
|
+
.describe('true = only tasks with NO due date; false = only tasks that have one; omit for no filter. ' +
|
|
391
|
+
'Cannot be combined with due_before / due_after when true.'),
|
|
392
|
+
no_assignee: z
|
|
393
|
+
.boolean()
|
|
394
|
+
.optional()
|
|
395
|
+
.describe('true = only tasks nobody is assigned to; false = only tasks somebody holds; omit for no filter. ' +
|
|
396
|
+
'A client person on a shared board, or a departed employee, still counts as holding a task. ' +
|
|
397
|
+
'Cannot be combined with assignee_employee_code when true.'),
|
|
398
|
+
custom_field: z
|
|
399
|
+
.record(fieldKey, z.union([z.string().min(1).max(500), z.number(), z.boolean()]))
|
|
400
|
+
.refine((m) => Object.keys(m).length >= 1 && Object.keys(m).length <= 20, {
|
|
401
|
+
message: 'custom_field must name 1-20 fields',
|
|
402
|
+
})
|
|
403
|
+
.optional()
|
|
404
|
+
.describe('Match custom field values: an object of field_key -> value, e.g. {"stage": "In build", "escalated": true}. ' +
|
|
405
|
+
'Several fields are ANDed. REQUIRES project_id (fields belong to one project); the keys are the ' +
|
|
406
|
+
'field_key values clokio_get_project_custom_fields lists, and a key that is not a public field of ' +
|
|
407
|
+
'that project is refused naming it. Per type: dropdown = the option value or its label (a label ' +
|
|
408
|
+
'two options share is refused asking for the value); multi-select = the task\'s list CONTAINS that ' +
|
|
409
|
+
'option; checkbox = true (ticked) or false (not ticked, an unanswered checkbox included); text, ' +
|
|
410
|
+
'url and email = exact match, ignoring case; number and date (YYYY-MM-DD) = exact.'),
|
|
312
411
|
open: z.boolean().optional().describe('true = only tasks whose status is not a "done" one'),
|
|
313
412
|
recurring: z
|
|
314
413
|
.boolean()
|
|
@@ -338,6 +437,11 @@ export function registerTaskTools(server, config) {
|
|
|
338
437
|
.string()
|
|
339
438
|
.optional()
|
|
340
439
|
.describe('ISO-8601. Incremental sync: only tasks changed at or after this instant'),
|
|
440
|
+
updated_before: z
|
|
441
|
+
.string()
|
|
442
|
+
.optional()
|
|
443
|
+
.describe('YYYY-MM-DD or ISO-8601, organization timezone. Only tasks last updated BEFORE this (exclusive); ' +
|
|
444
|
+
'the other half of updated_since'),
|
|
341
445
|
include_comments: z.boolean().optional(),
|
|
342
446
|
per_page: z.number().int().min(1).max(100).optional(),
|
|
343
447
|
page: z.number().int().optional(),
|
|
@@ -371,6 +475,7 @@ export function registerTaskTools(server, config) {
|
|
|
371
475
|
},
|
|
372
476
|
handler: (args, cfg) => request(cfg, '/tasks', {
|
|
373
477
|
query: {
|
|
478
|
+
...listFilterQuery(args),
|
|
374
479
|
project_id: args.project_id,
|
|
375
480
|
status: args.status,
|
|
376
481
|
priority: args.priority,
|
|
@@ -395,6 +500,83 @@ export function registerTaskTools(server, config) {
|
|
|
395
500
|
},
|
|
396
501
|
}),
|
|
397
502
|
});
|
|
503
|
+
registerTool(server, config, {
|
|
504
|
+
name: 'clokio_get_project_task_summary',
|
|
505
|
+
description: "One project's task numbers in one call, counted by the server: tasks by status (the project's own " +
|
|
506
|
+
'statuses, each with is_open), by priority (low / medium / high / urgent / none), by label name and by ' +
|
|
507
|
+
'assignee, plus unassigned and no_due_date counts; and two short lists of compact task rows: overdue ' +
|
|
508
|
+
"(open tasks due before today in the organisation's time zone, oldest due first) and stale (open tasks " +
|
|
509
|
+
'not updated for stale_days, least recently updated first), each at most 50 rows with a total. ' +
|
|
510
|
+
'OPEN tasks only unless include_closed is true. Subtasks count as tasks of their own unless top_level ' +
|
|
511
|
+
'is true. A task held by several people is counted for EACH of them in by_assignee, so those numbers ' +
|
|
512
|
+
'can sum above total. The answer carries a definitions block naming the rule behind every number, and ' +
|
|
513
|
+
'echoes stale_days. Use this for "how is project X doing" questions; use clokio_list_tasks for the rows ' +
|
|
514
|
+
'themselves. Needs tasks:read.',
|
|
515
|
+
schema: {
|
|
516
|
+
project_id: z.number().int(),
|
|
517
|
+
include_closed: z
|
|
518
|
+
.boolean()
|
|
519
|
+
.optional()
|
|
520
|
+
.describe('true also counts closed tasks (status category done). overdue and stale stay open tasks'),
|
|
521
|
+
top_level: z
|
|
522
|
+
.boolean()
|
|
523
|
+
.optional()
|
|
524
|
+
.describe('true counts top-level tasks only (subtasks excluded), overdue and stale included'),
|
|
525
|
+
stale_days: z
|
|
526
|
+
.number()
|
|
527
|
+
.int()
|
|
528
|
+
.min(1)
|
|
529
|
+
.max(365)
|
|
530
|
+
.optional()
|
|
531
|
+
.describe('Days without an update (updated_at) that make an open task stale. Default 14, range 1-365'),
|
|
532
|
+
group_by_custom_field: fieldKey
|
|
533
|
+
.optional()
|
|
534
|
+
.describe('The field_key of a PUBLIC dropdown or multi-select field of this project (clokio_get_project_custom_fields). ' +
|
|
535
|
+
'Adds by_custom_field: tasks per option (value and label) with open / closed / total, plus none for tasks ' +
|
|
536
|
+
'with no answer; a multi-select task counts under each option it holds. This answers "what is in each ' +
|
|
537
|
+
'release / sprint". Any other field type, a private field or an unknown key is refused (422).'),
|
|
538
|
+
},
|
|
539
|
+
handler: (args, cfg) => request(cfg, `/projects/${args.project_id}/task-summary`, {
|
|
540
|
+
query: {
|
|
541
|
+
include_closed: args.include_closed === undefined ? undefined : args.include_closed ? 1 : 0,
|
|
542
|
+
top_level: args.top_level === undefined ? undefined : args.top_level ? 1 : 0,
|
|
543
|
+
stale_days: args.stale_days,
|
|
544
|
+
group_by_custom_field: args.group_by_custom_field,
|
|
545
|
+
},
|
|
546
|
+
}),
|
|
547
|
+
});
|
|
548
|
+
registerTool(server, config, {
|
|
549
|
+
name: 'clokio_list_project_activity',
|
|
550
|
+
description: "One project's activity log, newest first: every change on every task of the board (deleted tasks " +
|
|
551
|
+
'included), one line per change naming the task, who acted (name, code, organisation) and what changed. ' +
|
|
552
|
+
"since / until narrow it (a plain date or time is read in the organisation's time zone; until is " +
|
|
553
|
+
'exclusive), action limits it to some actions (e.g. ["status_changed", "assigned"]). Pages of per_page ' +
|
|
554
|
+
'(1-100, default 25); the answer ends with the cursor to pass back for the next page, with the same ' +
|
|
555
|
+
"filters. Only PUBLIC custom fields' history appears, and internal comments never do. Use " +
|
|
556
|
+
'clokio_get_task_activity for one task. Needs tasks:read.',
|
|
557
|
+
schema: {
|
|
558
|
+
project_id: z.number().int(),
|
|
559
|
+
since: z.string().optional().describe('YYYY-MM-DD or ISO-8601: rows at or after this instant'),
|
|
560
|
+
until: z.string().optional().describe('YYYY-MM-DD or ISO-8601: rows before this instant (exclusive)'),
|
|
561
|
+
action: z
|
|
562
|
+
.array(z.string().min(1).max(60))
|
|
563
|
+
.min(1)
|
|
564
|
+
.max(30)
|
|
565
|
+
.optional()
|
|
566
|
+
.describe('Only these actions, e.g. ["status_changed", "assigned", "custom_field_changed"]'),
|
|
567
|
+
cursor: z.string().max(500).optional().describe('The cursor the previous page ended with'),
|
|
568
|
+
per_page: z.number().int().min(1).max(100).optional(),
|
|
569
|
+
},
|
|
570
|
+
handler: async (args, cfg) => renderProjectActivity(await request(cfg, `/projects/${args.project_id}/activities`, {
|
|
571
|
+
query: {
|
|
572
|
+
since: args.since,
|
|
573
|
+
until: args.until,
|
|
574
|
+
action: args.action,
|
|
575
|
+
cursor: args.cursor,
|
|
576
|
+
per_page: args.per_page,
|
|
577
|
+
},
|
|
578
|
+
})),
|
|
579
|
+
});
|
|
398
580
|
registerTool(server, config, {
|
|
399
581
|
name: 'clokio_get_task',
|
|
400
582
|
description: 'Get one task by id, with its full detail (assignees, labels, status, custom fields, task_url) and ' +
|
|
@@ -438,7 +620,7 @@ export function registerTaskTools(server, config) {
|
|
|
438
620
|
status: z.string().optional().describe("Status slug valid for THIS project's board"),
|
|
439
621
|
due_date: z.string().optional().describe('YYYY-MM-DD'),
|
|
440
622
|
start_date: z.string().optional().describe('YYYY-MM-DD'),
|
|
441
|
-
estimated_hours: z.number().min(0).max(
|
|
623
|
+
estimated_hours: z.number().min(0).max(ESTIMATED_HOURS_MAX).optional(),
|
|
442
624
|
parent_task_id: z.number().int().optional().describe('Makes this a subtask of that task'),
|
|
443
625
|
assignee_employee_codes: z
|
|
444
626
|
.array(assigneeReference)
|
|
@@ -510,7 +692,14 @@ export function registerTaskTools(server, config) {
|
|
|
510
692
|
'\n\n' +
|
|
511
693
|
'`custom_fields` (object field_key -> value) sets several custom fields in the same call, with the ' +
|
|
512
694
|
'rules of clokio_set_task_custom_field (option labels accepted, null clears). Every field is checked ' +
|
|
513
|
-
'before anything is written: one bad field refuses the whole update and nothing changes
|
|
695
|
+
'before anything is written: one bad field refuses the whole update and nothing changes.\n\n' +
|
|
696
|
+
'`parent_task_id` makes the task a subtask of another task on the SAME project (null makes it ' +
|
|
697
|
+
'top-level again). Subtasks go one level deep: a subtask cannot be a parent, a task that has subtasks ' +
|
|
698
|
+
'cannot become one, and a task cannot be its own parent.\n\n' +
|
|
699
|
+
'`assignee_employee_codes` (with `assignee_mode`: replace by default, add, remove) changes the ' +
|
|
700
|
+
'assignees in the same call, with exactly the rules of clokio_set_task_assignees (a client\'s person ' +
|
|
701
|
+
'as {employee_code, organization_id}; assigning makes the person a watcher). A refused person refuses ' +
|
|
702
|
+
'the WHOLE update: nothing in the call is written.',
|
|
514
703
|
schema: {
|
|
515
704
|
id: z.number().int(),
|
|
516
705
|
title: z.string().max(255).optional(),
|
|
@@ -519,8 +708,15 @@ export function registerTaskTools(server, config) {
|
|
|
519
708
|
status: z.string().optional().describe("Status slug valid for this task's board"),
|
|
520
709
|
due_date: z.string().nullable().optional().describe('YYYY-MM-DD, or null to clear'),
|
|
521
710
|
start_date: z.string().nullable().optional().describe('YYYY-MM-DD, or null to clear'),
|
|
522
|
-
estimated_hours: z.number().min(0).max(
|
|
523
|
-
project_id: z
|
|
711
|
+
estimated_hours: z.number().min(0).max(ESTIMATED_HOURS_MAX).nullable().optional(),
|
|
712
|
+
project_id: z
|
|
713
|
+
.number()
|
|
714
|
+
.int()
|
|
715
|
+
.optional()
|
|
716
|
+
.describe('Move the task to another project. Its subtasks move with it, and a status the new board lacks becomes ' +
|
|
717
|
+
'that board\'s default. A SUBTASK moves only when the same call sends parent_task_id: null (it ' +
|
|
718
|
+
'becomes a top-level task there) or the id of a task on the new project (it moves under it); ' +
|
|
719
|
+
'otherwise the server refuses it.'),
|
|
524
720
|
actor_employee_code: employeeCode.optional().describe('Who to credit in the activity log; omit to use the key issuer / CLOKIO_DEFAULT_ACTOR'),
|
|
525
721
|
recurrence: recurrenceInput
|
|
526
722
|
.nullable()
|
|
@@ -534,6 +730,23 @@ export function registerTaskTools(server, config) {
|
|
|
534
730
|
add_label_names: labelNames.describe('Labels to ADD, keeping the others. A missing name is CREATED; one the task already carries changes nothing'),
|
|
535
731
|
remove_label_names: labelNames.describe('Labels to REMOVE, keeping the others. A name the task does not carry, or that does not exist, changes nothing (never created)'),
|
|
536
732
|
custom_fields: customFieldsMap.optional(),
|
|
733
|
+
parent_task_id: z
|
|
734
|
+
.number()
|
|
735
|
+
.int()
|
|
736
|
+
.min(1)
|
|
737
|
+
.nullable()
|
|
738
|
+
.optional()
|
|
739
|
+
.describe('Hang the task under this task (same project, one level deep), or null to make it top-level'),
|
|
740
|
+
assignee_employee_codes: z
|
|
741
|
+
.array(assigneeReference)
|
|
742
|
+
.min(1)
|
|
743
|
+
.max(50)
|
|
744
|
+
.optional()
|
|
745
|
+
.describe('Assignees, as on clokio_set_task_assignees: a code of your organisation, or {employee_code, organization_id} for a client\'s person'),
|
|
746
|
+
assignee_mode: z
|
|
747
|
+
.enum(['replace', 'add', 'remove'])
|
|
748
|
+
.optional()
|
|
749
|
+
.describe('How assignee_employee_codes applies: replace (default) overwrites, add appends, remove takes off'),
|
|
537
750
|
},
|
|
538
751
|
handler: (args, cfg) => {
|
|
539
752
|
// Only keys the caller actually supplied may go in the body: PATCH
|
|
@@ -552,10 +765,16 @@ export function registerTaskTools(server, config) {
|
|
|
552
765
|
'actor_employee_code',
|
|
553
766
|
'label_names',
|
|
554
767
|
'custom_fields',
|
|
768
|
+
'parent_task_id',
|
|
769
|
+
'assignee_employee_codes',
|
|
770
|
+
'assignee_mode',
|
|
555
771
|
]) {
|
|
556
772
|
if (args[key] !== undefined)
|
|
557
773
|
body[key] = args[key];
|
|
558
774
|
}
|
|
775
|
+
if (args.assignee_mode !== undefined && args.assignee_employee_codes === undefined) {
|
|
776
|
+
throw new Error('assignee_mode needs assignee_employee_codes.');
|
|
777
|
+
}
|
|
559
778
|
refuseLabelConflicts(args, true);
|
|
560
779
|
// An empty add/remove list is not a change: it is not sent.
|
|
561
780
|
if (hasNames(args.add_label_names))
|
|
@@ -622,11 +841,15 @@ export function registerTaskTools(server, config) {
|
|
|
622
841
|
.max(50)
|
|
623
842
|
.describe('Each a code of your organisation, or {employee_code, organization_id} for a client\'s person on a shared board'),
|
|
624
843
|
mode: z.enum(['add', 'replace', 'remove']).describe('Required'),
|
|
844
|
+
actor_employee_code: employeeCode.optional().describe('Who to credit in the activity log; omit to use the key issuer / CLOKIO_DEFAULT_ACTOR'),
|
|
845
|
+
},
|
|
846
|
+
handler: async (args, cfg) => {
|
|
847
|
+
const body = { employee_codes: args.employee_codes, mode: args.mode };
|
|
848
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
849
|
+
if (actor)
|
|
850
|
+
body.actor_employee_code = actor;
|
|
851
|
+
return compactAssignees(args.id, await request(cfg, `/tasks/${args.id}/assignees`, { method: 'PATCH', body }), cfg.baseUrl);
|
|
625
852
|
},
|
|
626
|
-
handler: async (args, cfg) => compactAssignees(args.id, await request(cfg, `/tasks/${args.id}/assignees`, {
|
|
627
|
-
method: 'PATCH',
|
|
628
|
-
body: { employee_codes: args.employee_codes, mode: args.mode },
|
|
629
|
-
}), cfg.baseUrl),
|
|
630
853
|
});
|
|
631
854
|
registerTool(server, config, {
|
|
632
855
|
name: 'clokio_list_project_people',
|
|
@@ -945,7 +1168,7 @@ export function registerTaskTools(server, config) {
|
|
|
945
1168
|
'clokio_bulk_set_task_custom_field.',
|
|
946
1169
|
schema: {
|
|
947
1170
|
id: z.number().int(),
|
|
948
|
-
field_key:
|
|
1171
|
+
field_key: fieldKey,
|
|
949
1172
|
value: customFieldValue,
|
|
950
1173
|
actor_employee_code: employeeCode.optional().describe('Who to credit in the activity log; omit to use the key issuer / CLOKIO_DEFAULT_ACTOR'),
|
|
951
1174
|
},
|
|
@@ -985,11 +1208,12 @@ export function registerTaskTools(server, config) {
|
|
|
985
1208
|
registerTool(server, config, {
|
|
986
1209
|
name: 'clokio_bulk_update_tasks',
|
|
987
1210
|
mutates: true,
|
|
988
|
-
description: 'Apply ONE change (any of status, priority, due_date, add_label_names, remove_label_names) to up to 200 ' +
|
|
1211
|
+
description: 'Apply ONE change (any of status, priority, due_date, estimated_hours, add_label_names, remove_label_names) to up to 200 ' +
|
|
989
1212
|
'tasks at once - for cleaning up a board without a call per task. Partial success is normal: the result ' +
|
|
990
1213
|
'is {updated, not_found, invalid_status}. not_found lists ids that do not exist in your org (skipped, not ' +
|
|
991
1214
|
'fatal); invalid_status lists tasks whose board does not have the given status slug (such a task is ' +
|
|
992
|
-
'skipped whole, its labels included).
|
|
1215
|
+
'skipped whole, its labels included). estimated_hours (0-9999.99, null clears) sets one estimate on every ' +
|
|
1216
|
+
'task; a task already holding it changes nothing. Give at least one of status/priority/due_date/estimated_hours/add_label_names/' +
|
|
993
1217
|
'remove_label_names (an empty list does not count). Labels are a change only: add_label_names adds (a missing name is CREATED once in ' +
|
|
994
1218
|
'your organisation), remove_label_names removes (never creates); a label a task already carries, or one ' +
|
|
995
1219
|
'it does not carry, changes nothing for that task. To REPLACE a task\'s whole label set use ' +
|
|
@@ -1001,6 +1225,7 @@ export function registerTaskTools(server, config) {
|
|
|
1001
1225
|
status: z.string().optional().describe('Status slug - validated per task against its own board'),
|
|
1002
1226
|
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
|
|
1003
1227
|
due_date: z.string().nullable().optional().describe('YYYY-MM-DD, or null to clear'),
|
|
1228
|
+
estimated_hours: z.number().min(0).max(ESTIMATED_HOURS_MAX).nullable().optional().describe('Hours, 0-9999.99, set on every task; null clears'),
|
|
1004
1229
|
actor_employee_code: employeeCode.optional().describe('Who to credit; omit to use the key issuer'),
|
|
1005
1230
|
add_label_names: labelNames.describe('Labels to ADD to every task. A missing name is CREATED once in your organisation'),
|
|
1006
1231
|
remove_label_names: labelNames.describe('Labels to REMOVE from every task. An unknown name changes nothing (never created)'),
|
|
@@ -1009,9 +1234,10 @@ export function registerTaskTools(server, config) {
|
|
|
1009
1234
|
if (args.status === undefined &&
|
|
1010
1235
|
args.priority === undefined &&
|
|
1011
1236
|
args.due_date === undefined &&
|
|
1237
|
+
args.estimated_hours === undefined &&
|
|
1012
1238
|
!hasNames(args.add_label_names) &&
|
|
1013
1239
|
!hasNames(args.remove_label_names)) {
|
|
1014
|
-
throw new Error('Give at least one of: status, priority, due_date, add_label_names, remove_label_names.');
|
|
1240
|
+
throw new Error('Give at least one of: status, priority, due_date, estimated_hours, add_label_names, remove_label_names.');
|
|
1015
1241
|
}
|
|
1016
1242
|
refuseLabelConflicts(args, false);
|
|
1017
1243
|
const body = { ids: args.ids };
|
|
@@ -1021,6 +1247,8 @@ export function registerTaskTools(server, config) {
|
|
|
1021
1247
|
body.priority = args.priority;
|
|
1022
1248
|
if (args.due_date !== undefined)
|
|
1023
1249
|
body.due_date = args.due_date;
|
|
1250
|
+
if (args.estimated_hours !== undefined)
|
|
1251
|
+
body.estimated_hours = args.estimated_hours;
|
|
1024
1252
|
if (hasNames(args.add_label_names))
|
|
1025
1253
|
body.add_label_names = args.add_label_names;
|
|
1026
1254
|
if (hasNames(args.remove_label_names))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clokio-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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": {
|