clokio-mcp 0.1.0 → 0.1.1

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/dist/client.js CHANGED
@@ -15,6 +15,23 @@ export function loadConfig() {
15
15
  const baseUrl = (process.env.CLOKIO_BASE_URL?.trim() || 'https://app.clokio.io').replace(/\/+$/, '');
16
16
  return { baseUrl, apiKey };
17
17
  }
18
+ /**
19
+ * Encode ONE path segment supplied by a tool argument.
20
+ *
21
+ * Interpolating a raw argument into a request path lets it change which
22
+ * endpoint is called: `?` truncates the path (so a hardcoded `/status` suffix
23
+ * becomes query noise and the request lands on a different route), `/` adds
24
+ * segments, and `..` climbs. encodeURIComponent turns all of them into their
25
+ * percent-encoded forms, which the API then sees as a single literal value -
26
+ * and its own `[A-Za-z0-9_-]` route constraint rejects it.
27
+ *
28
+ * Every tool that puts a STRING into a path must wrap it in this. Numeric
29
+ * (z.number().int()) arguments cannot carry separators and need no wrapping,
30
+ * but passing them through is harmless.
31
+ */
32
+ export function seg(value) {
33
+ return encodeURIComponent(String(value));
34
+ }
18
35
  /** A Clokio API error, carrying the HTTP status and the server's message. */
19
36
  export class ClokioApiError extends Error {
20
37
  status;
@@ -35,6 +52,18 @@ export class ClokioApiError extends Error {
35
52
  export async function request(config, path, options = {}) {
36
53
  const { method = 'GET', query, body } = options;
37
54
  const url = new URL(`${config.baseUrl}/api/v1${path.startsWith('/') ? path : `/${path}`}`);
55
+ // A tool argument interpolated into `path` must never be able to change
56
+ // WHICH endpoint is called. new URL() normalises the assembled string, so a
57
+ // value containing `?` truncates the path (turning `/employees/X/status`
58
+ // into `/employees/X/pin?...`) and `../` segments climb out of /api/v1.
59
+ // That is an endpoint pivot: a caller who approved "set employee status"
60
+ // would instead reach PATCH /employees/{code}/pin, which resets a person's
61
+ // clock-in PIN and returns it in the response. Callers validate their own
62
+ // arguments, but this is the invariant that holds even if one forgets.
63
+ const expectedPrefix = new URL(config.baseUrl).pathname.replace(/\/+$/, '') + '/api/v1/';
64
+ if (!url.pathname.startsWith(expectedPrefix)) {
65
+ throw new ClokioApiError(0, 'Refusing to send a request outside /api/v1 - a path argument tried to change the endpoint.');
66
+ }
38
67
  if (query) {
39
68
  for (const [key, value] of Object.entries(query)) {
40
69
  if (value === undefined || value === null)
@@ -1,6 +1,10 @@
1
1
  import { z } from 'zod';
2
- import { request } from '../client.js';
2
+ import { request, seg } from '../client.js';
3
3
  import { registerTool } from './helpers.js';
4
+ /** See employees.ts - an employee_code reaches the request path. */
5
+ const employeeCode = z
6
+ .string()
7
+ .regex(/^[A-Za-z0-9_-]{1,50}$/, 'employee_code must be 1-50 chars of letters, digits, _ or -');
4
8
  /** Attendance reads + clock/break writes, plus time entries and balances. */
5
9
  export function registerAttendanceTools(server, config) {
6
10
  registerTool(server, config, {
@@ -22,11 +26,11 @@ export function registerAttendanceTools(server, config) {
22
26
  name: 'clokio_attendance_for_employee',
23
27
  description: "One employee's attendance. Optionally scope to a date range.",
24
28
  schema: {
25
- employee_code: z.string(),
29
+ employee_code: employeeCode,
26
30
  start_date: z.string().optional().describe('YYYY-MM-DD'),
27
31
  end_date: z.string().optional().describe('YYYY-MM-DD'),
28
32
  },
29
- handler: (args, cfg) => request(cfg, `/attendance/employee/${args.employee_code}`, {
33
+ handler: (args, cfg) => request(cfg, `/attendance/employee/${seg(args.employee_code)}`, {
30
34
  query: { start_date: args.start_date, end_date: args.end_date },
31
35
  }),
32
36
  });
@@ -1,6 +1,15 @@
1
1
  import { z } from 'zod';
2
- import { request } from '../client.js';
2
+ import { request, seg } from '../client.js';
3
3
  import { registerTool } from './helpers.js';
4
+ /**
5
+ * An employee_code goes into the request PATH, so it must not be able to carry
6
+ * a path separator or a query marker - see seg() / the /api/v1 guard in
7
+ * client.js. This mirrors the constraint the API route itself declares
8
+ * (`->where('employeeCode', '[A-Za-z0-9_-]{1,50}')`).
9
+ */
10
+ const employeeCode = z
11
+ .string()
12
+ .regex(/^[A-Za-z0-9_-]{1,50}$/, 'employee_code must be 1-50 chars of letters, digits, _ or -');
4
13
  /** Employee directory + provisioning. */
5
14
  export function registerEmployeeTools(server, config) {
6
15
  registerTool(server, config, {
@@ -31,10 +40,10 @@ export function registerEmployeeTools(server, config) {
31
40
  description: "One employee's open / delayed_from_open / past_due task counts. stale_days sets the 'delayed' threshold " +
32
41
  '(default 7, range 1-365). A task with N assignees counts for each of them.',
33
42
  schema: {
34
- employee_code: z.string(),
43
+ employee_code: employeeCode,
35
44
  stale_days: z.number().int().min(1).max(365).optional(),
36
45
  },
37
- handler: (args, cfg) => request(cfg, `/employees/${args.employee_code}/task-stats`, {
46
+ handler: (args, cfg) => request(cfg, `/employees/${seg(args.employee_code)}/task-stats`, {
38
47
  query: { stale_days: args.stale_days },
39
48
  }),
40
49
  });
@@ -60,10 +69,10 @@ export function registerEmployeeTools(server, config) {
60
69
  mutates: true,
61
70
  description: 'Set an employee\'s status (active / inactive / resign).',
62
71
  schema: {
63
- employee_code: z.string(),
72
+ employee_code: employeeCode,
64
73
  status: z.enum(['active', 'inactive', 'resign']),
65
74
  },
66
- handler: (args, cfg) => request(cfg, `/employees/${args.employee_code}/status`, {
75
+ handler: (args, cfg) => request(cfg, `/employees/${seg(args.employee_code)}/status`, {
67
76
  method: 'PATCH',
68
77
  body: { status: args.status },
69
78
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clokio-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Model Context Protocol (MCP) server for Clokio — manage tasks, projects, clients, employees and attendance from Claude and any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {