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 +21 -0
- package/README.md +107 -0
- package/dist/client.js +88 -0
- package/dist/index.js +38 -0
- package/dist/tools/attendance.js +107 -0
- package/dist/tools/employees.js +71 -0
- package/dist/tools/helpers.js +55 -0
- package/dist/tools/projects.js +72 -0
- package/dist/tools/tasks.js +189 -0
- package/package.json +52 -0
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
|
+
[](https://www.npmjs.com/package/clokio-mcp)
|
|
4
|
+
[](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
|
+
}
|