clokio-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matat Technologies LTD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Clokio MCP Server
2
+
3
+ [![npm version](https://img.shields.io/npm/v/clokio-mcp.svg)](https://www.npmjs.com/package/clokio-mcp)
4
+ [![MCP](https://img.shields.io/badge/MCP-compatible-blue)](https://modelcontextprotocol.io)
5
+
6
+ A [Model Context Protocol](https://modelcontextprotocol.io) server for the
7
+ **[Clokio](https://clokio.io) Public API**. Connect Claude — or any MCP client —
8
+ to your Clokio workspace to read and manage tasks, projects, clients, employees
9
+ and attendance in natural language.
10
+
11
+ Every request is authenticated with **your own** Clokio API key (`clk_...`),
12
+ sent as `X-API-Key`. The key carries its own scopes, so this server never
13
+ re-implements authorization: it is a thin, stateless client, and a `403` / `422`
14
+ from the API is surfaced verbatim.
15
+
16
+ ## Install
17
+
18
+ You need a Clokio API key. Create and scope keys in the admin dashboard at
19
+ **Settings → API Keys** (`/admin/api-keys/`). Each person uses their own key, so
20
+ permissions and the audit trail stay per-user.
21
+
22
+ ### Claude Code
23
+
24
+ ```bash
25
+ claude mcp add clokio \
26
+ --env CLOKIO_API_KEY=clk_your_key_here \
27
+ -- npx -y clokio-mcp
28
+ ```
29
+
30
+ ### Claude Desktop
31
+
32
+ Add to `claude_desktop_config.json`:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "clokio": {
38
+ "command": "npx",
39
+ "args": ["-y", "clokio-mcp"],
40
+ "env": { "CLOKIO_API_KEY": "clk_your_key_here" }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### Cursor / Cline / Zed / any MCP client
47
+
48
+ Use the same command — `npx -y clokio-mcp` with `CLOKIO_API_KEY` in the env.
49
+ MCP is an open standard.
50
+
51
+ Optional: set `CLOKIO_BASE_URL` to point at a different Clokio installation
52
+ (defaults to `https://app.clokio.io`).
53
+
54
+ ## Tools
55
+
56
+ 36 tools across five areas. Read-only tools carry the MCP `readOnlyHint`; writes
57
+ are marked mutating (deletes are `destructive`).
58
+
59
+ | Area | Tools |
60
+ |------|-------|
61
+ | **Tasks** | list, get, create, update, delete, set assignees, list/add comments, activity, list/add dependencies, set custom field |
62
+ | **Projects & clients** | list projects, project statuses, project custom fields, project contacts, get client, add client contact |
63
+ | **Reference** | task statuses, task labels, locations |
64
+ | **Employees** | list, lookup, task-stats, create, set status |
65
+ | **Attendance** | daily, range, per-employee, monthly summary, clock in/out, break start/end, time entries, balances |
66
+
67
+ ### Conventions
68
+
69
+ - **People are identified by `employee_code`** (e.g. `00080`), not internal ids.
70
+ Use `clokio_lookup_employee` to resolve a name to a code.
71
+ - **Attribute writes to a person** — pass `creator_employee_code` when creating a
72
+ task, `author_employee_code` when commenting, or the entry shows as the API key
73
+ owner ("External System").
74
+ - **Status slugs** come from `clokio_list_task_statuses` /
75
+ `clokio_get_project_statuses`.
76
+ - An API key sees **public custom fields only**.
77
+
78
+ ## Security
79
+
80
+ - The server contains **no secrets**. Your API key lives only in your local
81
+ environment (`CLOKIO_API_KEY`), never in this code or repository.
82
+ - The server **adds no permissions**. What a key can do is decided by the Clokio
83
+ API from that key's scopes — this server only forwards the request. Scope each
84
+ key to what its holder needs.
85
+ - All authorization, tenancy (`organization_id` scoping) and rate limiting are
86
+ enforced server-side by the Clokio API, exactly as for any other API consumer.
87
+
88
+ ## Local development
89
+
90
+ ```bash
91
+ git clone https://github.com/AmitMatat/clokio-mcp.git
92
+ cd clokio-mcp
93
+ npm install # includes devDependencies (typescript)
94
+ npm run build # rebuilds dist/
95
+ ```
96
+
97
+ The prebuilt `dist/` is committed so `npx` can run the server with no build step
98
+ (this also keeps it working when `NODE_ENV=production`, where npm skips
99
+ devDependencies). Rebuild and commit `dist/` alongside any source change.
100
+
101
+ The tool surface tracks the Clokio Public API v1; its OpenAPI spec is served
102
+ (unauthenticated) at `/api/v1/openapi.json` and is the source of truth for
103
+ request shapes.
104
+
105
+ ## License
106
+
107
+ MIT © Matat Technologies LTD
package/dist/client.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The ONE place that talks to the Clokio Public API.
3
+ *
4
+ * Every tool goes through request(); nothing else builds a fetch. Auth is the
5
+ * per-user `CLOKIO_API_KEY` sent as `X-API-Key`, exactly as the clokio-task CLI
6
+ * does. The key carries its own scopes, so this server never re-implements
7
+ * authorization — a 403/422 from the API is surfaced verbatim.
8
+ */
9
+ export function loadConfig() {
10
+ const apiKey = process.env.CLOKIO_API_KEY?.trim();
11
+ if (!apiKey) {
12
+ throw new Error('CLOKIO_API_KEY is not set. Provide your Clokio API key (clk_...) via the ' +
13
+ 'CLOKIO_API_KEY environment variable.');
14
+ }
15
+ const baseUrl = (process.env.CLOKIO_BASE_URL?.trim() || 'https://app.clokio.io').replace(/\/+$/, '');
16
+ return { baseUrl, apiKey };
17
+ }
18
+ /** A Clokio API error, carrying the HTTP status and the server's message. */
19
+ export class ClokioApiError extends Error {
20
+ status;
21
+ body;
22
+ constructor(status, message, body) {
23
+ super(message);
24
+ this.status = status;
25
+ this.body = body;
26
+ this.name = 'ClokioApiError';
27
+ }
28
+ }
29
+ /**
30
+ * Perform a Clokio API v1 request under `/api/v1`. Returns the parsed `data`
31
+ * field of the standard `{status, message, data}` envelope when present, or the
32
+ * whole parsed body otherwise. Throws ClokioApiError on a non-2xx response with
33
+ * the server's own message.
34
+ */
35
+ export async function request(config, path, options = {}) {
36
+ const { method = 'GET', query, body } = options;
37
+ const url = new URL(`${config.baseUrl}/api/v1${path.startsWith('/') ? path : `/${path}`}`);
38
+ if (query) {
39
+ for (const [key, value] of Object.entries(query)) {
40
+ if (value === undefined || value === null)
41
+ continue;
42
+ if (Array.isArray(value)) {
43
+ for (const v of value)
44
+ url.searchParams.append(`${key}[]`, String(v));
45
+ }
46
+ else {
47
+ url.searchParams.set(key, String(value));
48
+ }
49
+ }
50
+ }
51
+ const headers = {
52
+ 'X-API-Key': config.apiKey,
53
+ Accept: 'application/json',
54
+ };
55
+ let payload;
56
+ if (body !== undefined) {
57
+ headers['Content-Type'] = 'application/json';
58
+ payload = JSON.stringify(body);
59
+ }
60
+ let res;
61
+ try {
62
+ res = await fetch(url, { method, headers, body: payload });
63
+ }
64
+ catch (e) {
65
+ throw new ClokioApiError(0, `Network error reaching Clokio: ${e.message}`);
66
+ }
67
+ const text = await res.text();
68
+ let parsed = undefined;
69
+ if (text) {
70
+ try {
71
+ parsed = JSON.parse(text);
72
+ }
73
+ catch {
74
+ parsed = text;
75
+ }
76
+ }
77
+ if (!res.ok) {
78
+ const message = (parsed && typeof parsed === 'object' && 'message' in parsed && typeof parsed.message === 'string'
79
+ ? parsed.message
80
+ : `Clokio API returned HTTP ${res.status}`);
81
+ throw new ClokioApiError(res.status, message, parsed);
82
+ }
83
+ // Standard envelope: {status, message, data}. Unwrap `data` when present.
84
+ if (parsed && typeof parsed === 'object' && 'data' in parsed) {
85
+ return parsed.data;
86
+ }
87
+ return parsed;
88
+ }
package/dist/index.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * clokio-mcp — an MCP server for the Clokio Public API.
4
+ *
5
+ * Exposes tasks, projects/clients, employees and attendance as MCP tools over
6
+ * stdio. Auth is the per-user CLOKIO_API_KEY (an X-API-Key credential); the key
7
+ * carries its own scopes, so this server never re-implements authorization.
8
+ *
9
+ * Run: CLOKIO_API_KEY=clk_... node dist/index.js
10
+ * Or register with: claude mcp add clokio -- npx -y clokio-mcp
11
+ * (set CLOKIO_API_KEY, and optionally CLOKIO_BASE_URL, in the MCP env).
12
+ */
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { loadConfig } from './client.js';
16
+ import { registerTaskTools } from './tools/tasks.js';
17
+ import { registerProjectTools } from './tools/projects.js';
18
+ import { registerEmployeeTools } from './tools/employees.js';
19
+ import { registerAttendanceTools } from './tools/attendance.js';
20
+ async function main() {
21
+ const config = loadConfig();
22
+ const server = new McpServer({
23
+ name: 'clokio-mcp',
24
+ version: '0.1.0',
25
+ });
26
+ registerTaskTools(server, config);
27
+ registerProjectTools(server, config);
28
+ registerEmployeeTools(server, config);
29
+ registerAttendanceTools(server, config);
30
+ const transport = new StdioServerTransport();
31
+ await server.connect(transport);
32
+ // stderr is safe for logs on stdio transport (stdout carries the protocol).
33
+ process.stderr.write(`clokio-mcp connected (base: ${config.baseUrl})\n`);
34
+ }
35
+ main().catch((e) => {
36
+ process.stderr.write(`clokio-mcp failed to start: ${e.message}\n`);
37
+ process.exit(1);
38
+ });
@@ -0,0 +1,107 @@
1
+ import { z } from 'zod';
2
+ import { request } from '../client.js';
3
+ import { registerTool } from './helpers.js';
4
+ /** Attendance reads + clock/break writes, plus time entries and balances. */
5
+ export function registerAttendanceTools(server, config) {
6
+ registerTool(server, config, {
7
+ name: 'clokio_attendance_daily',
8
+ description: 'Attendance for a single date (YYYY-MM-DD, defaults to today).',
9
+ schema: { date: z.string().optional().describe('YYYY-MM-DD') },
10
+ handler: (args, cfg) => request(cfg, '/attendance/daily', { query: { date: args.date } }),
11
+ });
12
+ registerTool(server, config, {
13
+ name: 'clokio_attendance_range',
14
+ description: 'Attendance for a date range (max 31 days).',
15
+ schema: {
16
+ start_date: z.string().describe('YYYY-MM-DD'),
17
+ end_date: z.string().describe('YYYY-MM-DD'),
18
+ },
19
+ handler: (args, cfg) => request(cfg, '/attendance/range', { query: { start_date: args.start_date, end_date: args.end_date } }),
20
+ });
21
+ registerTool(server, config, {
22
+ name: 'clokio_attendance_for_employee',
23
+ description: "One employee's attendance. Optionally scope to a date range.",
24
+ schema: {
25
+ employee_code: z.string(),
26
+ start_date: z.string().optional().describe('YYYY-MM-DD'),
27
+ end_date: z.string().optional().describe('YYYY-MM-DD'),
28
+ },
29
+ handler: (args, cfg) => request(cfg, `/attendance/employee/${args.employee_code}`, {
30
+ query: { start_date: args.start_date, end_date: args.end_date },
31
+ }),
32
+ });
33
+ registerTool(server, config, {
34
+ name: 'clokio_attendance_summary',
35
+ description: 'Monthly attendance summary.',
36
+ schema: {
37
+ month: z.string().optional().describe('YYYY-MM'),
38
+ },
39
+ handler: (args, cfg) => request(cfg, '/attendance/summary', { query: { month: args.month } }),
40
+ });
41
+ registerTool(server, config, {
42
+ name: 'clokio_clock_in',
43
+ mutates: true,
44
+ description: 'Clock an employee in. A PIN may be required depending on org configuration. Timestamps are clamped to ' +
45
+ '+/-10 minutes of server time.',
46
+ schema: {
47
+ employee_code: z.string(),
48
+ pin: z.string().optional(),
49
+ location_id: z.number().int().optional(),
50
+ },
51
+ handler: (args, cfg) => request(cfg, '/attendance/clock-in', {
52
+ method: 'POST',
53
+ body: { employee_code: args.employee_code, pin: args.pin, location_id: args.location_id },
54
+ }),
55
+ });
56
+ registerTool(server, config, {
57
+ name: 'clokio_clock_out',
58
+ mutates: true,
59
+ description: 'Clock an employee out.',
60
+ schema: {
61
+ employee_code: z.string(),
62
+ pin: z.string().optional(),
63
+ },
64
+ handler: (args, cfg) => request(cfg, '/attendance/clock-out', {
65
+ method: 'POST',
66
+ body: { employee_code: args.employee_code, pin: args.pin },
67
+ }),
68
+ });
69
+ registerTool(server, config, {
70
+ name: 'clokio_break_start',
71
+ mutates: true,
72
+ description: 'Start a break for an employee.',
73
+ schema: { employee_code: z.string(), pin: z.string().optional() },
74
+ handler: (args, cfg) => request(cfg, '/attendance/break-start', {
75
+ method: 'POST',
76
+ body: { employee_code: args.employee_code, pin: args.pin },
77
+ }),
78
+ });
79
+ registerTool(server, config, {
80
+ name: 'clokio_break_end',
81
+ mutates: true,
82
+ description: 'End a break for an employee.',
83
+ schema: { employee_code: z.string(), pin: z.string().optional() },
84
+ handler: (args, cfg) => request(cfg, '/attendance/break-end', {
85
+ method: 'POST',
86
+ body: { employee_code: args.employee_code, pin: args.pin },
87
+ }),
88
+ });
89
+ registerTool(server, config, {
90
+ name: 'clokio_list_time_entries',
91
+ description: 'List time-tracking entries. Filterable by employee_code and date range.',
92
+ schema: {
93
+ employee_code: z.string().optional(),
94
+ start_date: z.string().optional().describe('YYYY-MM-DD'),
95
+ end_date: z.string().optional().describe('YYYY-MM-DD'),
96
+ },
97
+ handler: (args, cfg) => request(cfg, '/time-entries', {
98
+ query: { employee_code: args.employee_code, start_date: args.start_date, end_date: args.end_date },
99
+ }),
100
+ });
101
+ registerTool(server, config, {
102
+ name: 'clokio_get_balances',
103
+ description: 'Leave / hour balances.',
104
+ schema: { employee_code: z.string().optional() },
105
+ handler: (args, cfg) => request(cfg, '/balances', { query: { employee_code: args.employee_code } }),
106
+ });
107
+ }
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+ import { request } from '../client.js';
3
+ import { registerTool } from './helpers.js';
4
+ /** Employee directory + provisioning. */
5
+ export function registerEmployeeTools(server, config) {
6
+ registerTool(server, config, {
7
+ name: 'clokio_list_employees',
8
+ description: 'List employees. Returns employee_code (the stable id used everywhere else, e.g. for assignees), name, ' +
9
+ 'and non-PII fields. email is only returned if the key holds the employees:pii scope.',
10
+ schema: {
11
+ search: z.string().optional().describe('Name filter'),
12
+ page: z.number().int().optional(),
13
+ },
14
+ handler: (args, cfg) => request(cfg, '/employees', { query: { search: args.search, page: args.page } }),
15
+ });
16
+ registerTool(server, config, {
17
+ name: 'clokio_lookup_employee',
18
+ description: 'Look up an employee by name or email to resolve their employee_code. Use this before assigning a task ' +
19
+ 'when you only know the person\'s name.',
20
+ schema: {
21
+ name: z.string().optional(),
22
+ email: z.string().optional(),
23
+ },
24
+ handler: (args, cfg) => request(cfg, '/employees/lookup', {
25
+ method: 'POST',
26
+ body: { name: args.name, email: args.email },
27
+ }),
28
+ });
29
+ registerTool(server, config, {
30
+ name: 'clokio_get_employee_task_stats',
31
+ description: "One employee's open / delayed_from_open / past_due task counts. stale_days sets the 'delayed' threshold " +
32
+ '(default 7, range 1-365). A task with N assignees counts for each of them.',
33
+ schema: {
34
+ employee_code: z.string(),
35
+ stale_days: z.number().int().min(1).max(365).optional(),
36
+ },
37
+ handler: (args, cfg) => request(cfg, `/employees/${args.employee_code}/task-stats`, {
38
+ query: { stale_days: args.stale_days },
39
+ }),
40
+ });
41
+ registerTool(server, config, {
42
+ name: 'clokio_create_employee',
43
+ mutates: true,
44
+ description: 'Create (or reactivate) an employee. Reactivating an existing inactive employee never resets their ' +
45
+ 'password. A pin (4-6 digits, org-unique) can be supplied or is auto-generated; it is returned only here.',
46
+ schema: {
47
+ name: z.string(),
48
+ email: z.string().optional(),
49
+ phone: z.string().optional(),
50
+ location_id: z.number().int().optional(),
51
+ pin: z.string().optional(),
52
+ },
53
+ handler: (args, cfg) => request(cfg, '/employees', {
54
+ method: 'POST',
55
+ body: { name: args.name, email: args.email, phone: args.phone, location_id: args.location_id, pin: args.pin },
56
+ }),
57
+ });
58
+ registerTool(server, config, {
59
+ name: 'clokio_set_employee_status',
60
+ mutates: true,
61
+ description: 'Set an employee\'s status (active / inactive / resign).',
62
+ schema: {
63
+ employee_code: z.string(),
64
+ status: z.enum(['active', 'inactive', 'resign']),
65
+ },
66
+ handler: (args, cfg) => request(cfg, `/employees/${args.employee_code}/status`, {
67
+ method: 'PATCH',
68
+ body: { status: args.status },
69
+ }),
70
+ });
71
+ }
@@ -0,0 +1,55 @@
1
+ import { ClokioApiError } from '../client.js';
2
+ /**
3
+ * Register one Clokio tool. The handler returns any JSON-serialisable value,
4
+ * rendered as pretty JSON text. ClokioApiError becomes an isError tool result
5
+ * carrying the API's own status + message, so the model can correct its request
6
+ * (a 422 naming the bad field, a 404, a 403 scope refusal).
7
+ *
8
+ * `mutates` marks a tool that changes data (create/update/delete): it sets the
9
+ * MCP readOnlyHint/destructiveHint annotations so clients can surface a warning.
10
+ */
11
+ export function registerTool(server, config, def) {
12
+ const callback = async (args) => {
13
+ try {
14
+ const result = await def.handler(args, config);
15
+ return {
16
+ content: [
17
+ {
18
+ type: 'text',
19
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
20
+ },
21
+ ],
22
+ };
23
+ }
24
+ catch (e) {
25
+ if (e instanceof ClokioApiError) {
26
+ return {
27
+ isError: true,
28
+ content: [
29
+ {
30
+ type: 'text',
31
+ text: `Clokio API error (HTTP ${e.status}): ${e.message}` +
32
+ (e.body ? `\n\nDetails:\n${JSON.stringify(e.body, null, 2)}` : ''),
33
+ },
34
+ ],
35
+ };
36
+ }
37
+ return {
38
+ isError: true,
39
+ content: [{ type: 'text', text: `Error: ${e.message}` }],
40
+ };
41
+ }
42
+ };
43
+ server.registerTool(def.name, {
44
+ description: def.description,
45
+ inputSchema: def.schema,
46
+ annotations: {
47
+ readOnlyHint: !def.mutates,
48
+ destructiveHint: def.mutates === 'destructive',
49
+ },
50
+ },
51
+ // The SDK's callback type is an inline structural shape that our
52
+ // CallToolResult return does not unify with under strict generics; the
53
+ // runtime shape is correct, so the cast is the pragmatic bridge.
54
+ callback);
55
+ }
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import { request } from '../client.js';
3
+ import { registerTool } from './helpers.js';
4
+ /** Projects, clients, and the board-configuration + reference lookups. */
5
+ export function registerProjectTools(server, config) {
6
+ registerTool(server, config, {
7
+ name: 'clokio_list_projects',
8
+ description: 'List projects, optionally filtered by a search term (name / client).',
9
+ schema: { search: z.string().optional() },
10
+ handler: (args, cfg) => request(cfg, '/projects', { query: { search: args.search } }),
11
+ });
12
+ registerTool(server, config, {
13
+ name: 'clokio_get_project_statuses',
14
+ description: "A project's resolved task-status set, in order (what a task in this project may become).",
15
+ schema: { project_id: z.number().int() },
16
+ handler: (args, cfg) => request(cfg, `/projects/${args.project_id}/statuses`),
17
+ });
18
+ registerTool(server, config, {
19
+ name: 'clokio_get_project_custom_fields',
20
+ description: "A project's custom-field definitions, in sort order. An API key sees PUBLIC fields only.",
21
+ schema: { project_id: z.number().int() },
22
+ handler: (args, cfg) => request(cfg, `/projects/${args.project_id}/custom-fields`),
23
+ });
24
+ registerTool(server, config, {
25
+ name: 'clokio_get_project_contacts',
26
+ description: "The contacts for a project's client.",
27
+ schema: { project_id: z.number().int() },
28
+ handler: (args, cfg) => request(cfg, `/projects/${args.project_id}/contacts`),
29
+ });
30
+ registerTool(server, config, {
31
+ name: 'clokio_get_client',
32
+ description: 'Get a client by id.',
33
+ schema: { client_id: z.number().int() },
34
+ handler: (args, cfg) => request(cfg, `/clients/${args.client_id}`),
35
+ });
36
+ registerTool(server, config, {
37
+ name: 'clokio_add_client_contact',
38
+ mutates: true,
39
+ description: 'Add a contact to a client.',
40
+ schema: {
41
+ client_id: z.number().int(),
42
+ name: z.string(),
43
+ email: z.string().optional(),
44
+ phone: z.string().optional(),
45
+ role: z.string().optional(),
46
+ },
47
+ handler: (args, cfg) => request(cfg, `/clients/${args.client_id}/contacts`, {
48
+ method: 'POST',
49
+ body: { name: args.name, email: args.email, phone: args.phone, role: args.role },
50
+ }),
51
+ });
52
+ // ── Reference / vocabulary lookups ──
53
+ registerTool(server, config, {
54
+ name: 'clokio_list_task_statuses',
55
+ description: 'The organisation-wide task-status vocabulary, with is_open per slug (including project-specific slugs). ' +
56
+ 'Use this to discover valid status slugs before creating/updating a task.',
57
+ schema: {},
58
+ handler: (_args, cfg) => request(cfg, '/task-statuses'),
59
+ });
60
+ registerTool(server, config, {
61
+ name: 'clokio_list_task_labels',
62
+ description: 'List the available task labels (tags), with names and colors.',
63
+ schema: {},
64
+ handler: (_args, cfg) => request(cfg, '/task-labels'),
65
+ });
66
+ registerTool(server, config, {
67
+ name: 'clokio_list_locations',
68
+ description: 'List the organisation\'s locations.',
69
+ schema: {},
70
+ handler: (_args, cfg) => request(cfg, '/locations'),
71
+ });
72
+ }
@@ -0,0 +1,189 @@
1
+ import { z } from 'zod';
2
+ import { request } from '../client.js';
3
+ import { registerTool } from './helpers.js';
4
+ /** Task-management tools: the core of the Clokio API surface. */
5
+ export function registerTaskTools(server, config) {
6
+ registerTool(server, config, {
7
+ name: 'clokio_list_tasks',
8
+ description: 'List tasks. Supports filters: project_id, status (slug), priority, assignee (employee_code), ' +
9
+ 'label, search, open (1 = only open tasks), created_before / due_before (YYYY-MM-DD, org timezone). ' +
10
+ 'Paginate with page, or crawl with cursor. Each task carries a task_url.',
11
+ schema: {
12
+ project_id: z.number().int().optional(),
13
+ status: z.string().optional().describe('Status slug, e.g. "in_progress"'),
14
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
15
+ assignee: z.string().optional().describe('employee_code, e.g. "00080"'),
16
+ label: z.string().optional(),
17
+ search: z.string().optional(),
18
+ open: z.boolean().optional().describe('true = only tasks whose status is not "done"'),
19
+ created_before: z.string().optional().describe('YYYY-MM-DD'),
20
+ due_before: z.string().optional().describe('YYYY-MM-DD'),
21
+ page: z.number().int().optional(),
22
+ cursor: z.string().optional(),
23
+ },
24
+ handler: (args, cfg) => request(cfg, '/tasks', {
25
+ query: {
26
+ project_id: args.project_id,
27
+ status: args.status,
28
+ priority: args.priority,
29
+ assignee: args.assignee,
30
+ label: args.label,
31
+ search: args.search,
32
+ open: args.open ? 1 : undefined,
33
+ created_before: args.created_before,
34
+ due_before: args.due_before,
35
+ page: args.page,
36
+ cursor: args.cursor,
37
+ },
38
+ }),
39
+ });
40
+ registerTool(server, config, {
41
+ name: 'clokio_get_task',
42
+ description: 'Get one task by id, with its full detail (assignees, labels, status, custom fields, task_url).',
43
+ schema: { id: z.number().int() },
44
+ handler: (args, cfg) => request(cfg, `/tasks/${args.id}`),
45
+ });
46
+ registerTool(server, config, {
47
+ name: 'clokio_create_task',
48
+ mutates: true,
49
+ description: 'Create a task in a project. assignees/creator use employee_code (e.g. "00080"), not internal ids. ' +
50
+ 'To attribute the task to a person set creator_employee_code, otherwise it shows as the API key owner.',
51
+ schema: {
52
+ project_id: z.number().int(),
53
+ title: z.string(),
54
+ description: z.string().optional().describe('HTML or plain text'),
55
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
56
+ status: z.string().optional().describe('Status slug'),
57
+ due_date: z.string().optional().describe('YYYY-MM-DD'),
58
+ assignee_employee_codes: z.array(z.string()).optional(),
59
+ label_names: z.array(z.string()).optional(),
60
+ creator_employee_code: z.string().optional(),
61
+ },
62
+ handler: (args, cfg) => request(cfg, '/tasks', {
63
+ method: 'POST',
64
+ body: {
65
+ project_id: args.project_id,
66
+ title: args.title,
67
+ description: args.description,
68
+ priority: args.priority,
69
+ status: args.status,
70
+ due_date: args.due_date,
71
+ assignees: args.assignee_employee_codes,
72
+ labels: args.label_names,
73
+ created_by_employee_code: args.creator_employee_code,
74
+ },
75
+ }),
76
+ });
77
+ registerTool(server, config, {
78
+ name: 'clokio_update_task',
79
+ mutates: true,
80
+ description: 'Update a task. Only the fields you pass are changed. status/priority/title/description/due_date are supported.',
81
+ schema: {
82
+ id: z.number().int(),
83
+ title: z.string().optional(),
84
+ description: z.string().optional(),
85
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
86
+ status: z.string().optional().describe('Status slug'),
87
+ due_date: z.string().optional().describe('YYYY-MM-DD, or empty string to clear'),
88
+ },
89
+ handler: (args, cfg) => {
90
+ const body = {};
91
+ if (args.title !== undefined)
92
+ body.title = args.title;
93
+ if (args.description !== undefined)
94
+ body.description = args.description;
95
+ if (args.priority !== undefined)
96
+ body.priority = args.priority;
97
+ if (args.status !== undefined)
98
+ body.status = args.status;
99
+ if (args.due_date !== undefined)
100
+ body.due_date = args.due_date;
101
+ return request(cfg, `/tasks/${args.id}`, { method: 'PATCH', body });
102
+ },
103
+ });
104
+ registerTool(server, config, {
105
+ name: 'clokio_delete_task',
106
+ mutates: 'destructive',
107
+ description: 'Delete a task by id. This is irreversible.',
108
+ schema: { id: z.number().int() },
109
+ handler: (args, cfg) => request(cfg, `/tasks/${args.id}`, { method: 'DELETE' }),
110
+ });
111
+ registerTool(server, config, {
112
+ name: 'clokio_set_task_assignees',
113
+ mutates: true,
114
+ description: 'Set a task\'s assignees. mode "add" appends, "replace" overwrites, "remove" removes. ' +
115
+ 'Assignees are employee_codes (e.g. "00080").',
116
+ schema: {
117
+ id: z.number().int(),
118
+ employee_codes: z.array(z.string()),
119
+ mode: z.enum(['add', 'replace', 'remove']).optional().describe('default: add'),
120
+ },
121
+ handler: (args, cfg) => request(cfg, `/tasks/${args.id}/assignees`, {
122
+ method: 'PATCH',
123
+ body: { assignees: args.employee_codes, mode: args.mode ?? 'add' },
124
+ }),
125
+ });
126
+ registerTool(server, config, {
127
+ name: 'clokio_list_task_comments',
128
+ description: 'List the comments on a task.',
129
+ schema: { id: z.number().int() },
130
+ handler: (args, cfg) => request(cfg, `/tasks/${args.id}/comments`),
131
+ });
132
+ registerTool(server, config, {
133
+ name: 'clokio_add_task_comment',
134
+ mutates: true,
135
+ description: 'Add a comment to a task. Set author_employee_code to attribute it to a person; otherwise it shows as ' +
136
+ 'the API key owner ("External System"). Use readable formatting (paragraphs, bullet lists); markdown ' +
137
+ 'tables are not rendered.',
138
+ schema: {
139
+ id: z.number().int(),
140
+ body: z.string(),
141
+ author_employee_code: z.string().optional(),
142
+ },
143
+ handler: (args, cfg) => request(cfg, `/tasks/${args.id}/comments`, {
144
+ method: 'POST',
145
+ body: { body: args.body, author_employee_code: args.author_employee_code },
146
+ }),
147
+ });
148
+ registerTool(server, config, {
149
+ name: 'clokio_get_task_activity',
150
+ description: 'Get the activity log (audit trail) for a task.',
151
+ schema: { id: z.number().int() },
152
+ handler: (args, cfg) => request(cfg, `/tasks/${args.id}/activity`),
153
+ });
154
+ registerTool(server, config, {
155
+ name: 'clokio_list_task_dependencies',
156
+ description: 'List a task\'s dependencies (blocks / waiting-on).',
157
+ schema: { id: z.number().int() },
158
+ handler: (args, cfg) => request(cfg, `/tasks/${args.id}/dependencies`),
159
+ });
160
+ registerTool(server, config, {
161
+ name: 'clokio_add_task_dependency',
162
+ mutates: true,
163
+ description: 'Add a dependency between two tasks. Circular dependencies are rejected by the API.',
164
+ schema: {
165
+ id: z.number().int().describe('The task that depends'),
166
+ depends_on_task_id: z.number().int(),
167
+ type: z.enum(['blocks', 'waiting_on']).optional(),
168
+ },
169
+ handler: (args, cfg) => request(cfg, `/tasks/${args.id}/dependencies`, {
170
+ method: 'POST',
171
+ body: { depends_on_task_id: args.depends_on_task_id, type: args.type },
172
+ }),
173
+ });
174
+ registerTool(server, config, {
175
+ name: 'clokio_set_task_custom_field',
176
+ mutates: true,
177
+ description: 'Set one custom field value on a task, identified by field_key (never field_id). An API key sees ' +
178
+ 'PUBLIC fields only.',
179
+ schema: {
180
+ id: z.number().int(),
181
+ field_key: z.string(),
182
+ value: z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]),
183
+ },
184
+ handler: (args, cfg) => request(cfg, `/tasks/${args.id}/custom-fields`, {
185
+ method: 'PATCH',
186
+ body: { field_key: args.field_key, value: args.value },
187
+ }),
188
+ });
189
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "clokio-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Model Context Protocol (MCP) server for Clokio — manage tasks, projects, clients, employees and attendance from Claude and any MCP client.",
5
+ "type": "module",
6
+ "bin": {
7
+ "clokio-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsc --watch",
17
+ "start": "node dist/index.js",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "clokio",
24
+ "claude",
25
+ "ai",
26
+ "attendance",
27
+ "task-management",
28
+ "project-management",
29
+ "time-tracking"
30
+ ],
31
+ "author": "Matat Technologies LTD",
32
+ "license": "MIT",
33
+ "homepage": "https://clokio.io",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/AmitMatat/clokio-mcp.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/AmitMatat/clokio-mcp/issues"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.0.4",
43
+ "zod": "^3.23.8"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.20.4",
47
+ "typescript": "^5.9.3"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ }
52
+ }