viviscape-mcp 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -144,6 +144,12 @@ work, so each list tool accepts:
144
144
  - `fields` - columns to return; omit for a curated default, or pass `["all"]`
145
145
  - `limit` / `offset` - page window, default 50 rows
146
146
 
147
+ Task rows also carry computed `due_date`, `due_in_days` (negative when
148
+ overdue), and `overdue`, derived from the task's `end` date. Read those rather
149
+ than the API's `deadline` string, which does not track the due date -- it
150
+ reported "In 2 Days" for a task 39 days overdue. `deadline` is excluded from the
151
+ default field set; ask for it explicitly if you need to see what the UI shows.
152
+
147
153
  Responses are wrapped as `{ total, returned, offset, next_offset, fields, items }`
148
154
  so a caller can tell when more rows exist. Task lists also accept
149
155
  `company_id`, `project_id`, `assignee_id`, `status`, `priority`,
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { ViviScapeClient } from './api-client.js';
7
7
  import { AuthService } from './auth/auth-service.js';
8
8
  import { describe } from './auth/credentials.js';
9
9
  import { PRIORITIES, PROJECT_STATUSES, PROSPECT_STATUSES, TASK_STATUSES, enumReference, } from './enums.js';
10
- import { FILE_FIELDS, NOTE_FIELDS, PROJECT_FIELDS, TASK_FIELDS, filterTasks, shape, } from './projection.js';
10
+ import { FILE_FIELDS, NOTE_FIELDS, PROJECT_FIELDS, TASK_FIELDS, annotateTasks, filterTasks, shape, } from './projection.js';
11
11
  import { baseUrl } from './config.js';
12
12
  // quiet: dotenv's banner goes to stdout, which is the MCP protocol channel.
13
13
  config({ quiet: true });
@@ -336,11 +336,11 @@ server.tool('project_tasks', 'Get all tasks for a project. Returns a page of tri
336
336
  // project_id is the query, not a filter -- re-filtering on it would drop
337
337
  // every row if the response names the column differently.
338
338
  const rows = Array.isArray(result)
339
- ? filterTasks(result, { ...params, project_id: undefined })
339
+ ? annotateTasks(filterTasks(result, { ...params, project_id: undefined }))
340
340
  : result;
341
341
  return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
342
342
  });
343
- server.tool('tasks_open', 'Get open tasks across projects. company_id and only_mine filter server-side; the rest filter in-process. Returns a page of trimmed rows.', {
343
+ server.tool('tasks_open', 'Get open tasks across projects. company_id and only_mine filter server-side; the rest filter in-process. Returns a page of trimmed rows with computed due_date / due_in_days / overdue -- read those, not the API deadline string, which is unreliable.', {
344
344
  only_mine: z.boolean().optional().describe('Only tasks assigned to the signed-in user (server-side)'),
345
345
  team: z.string().optional().describe('Team filter passed through to the API'),
346
346
  ...taskFilterArgs,
@@ -351,7 +351,7 @@ server.tool('tasks_open', 'Get open tasks across projects. company_id and only_m
351
351
  only_mine: params.only_mine,
352
352
  team: params.team,
353
353
  });
354
- const rows = Array.isArray(result) ? filterTasks(result, params) : result;
354
+ const rows = Array.isArray(result) ? annotateTasks(filterTasks(result, params)) : result;
355
355
  return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
356
356
  });
357
357
  server.tool('task_add', 'Add a task to a project. The API requires a full Group_Task payload; this tool builds it from simplified params.', {
@@ -394,7 +394,7 @@ server.tool('tasks_pending', 'Get pending tasks for a user. Returns a page of tr
394
394
  ...taskFilterArgs,
395
395
  }, async (params) => {
396
396
  const result = await requireClient().getPendingTasks(params.user_id);
397
- const rows = Array.isArray(result) ? filterTasks(result, params) : result;
397
+ const rows = Array.isArray(result) ? annotateTasks(filterTasks(result, params)) : result;
398
398
  return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
399
399
  });
400
400
  server.tool('tasks_by_milestone', 'Get tasks for a milestone. Returns a page of trimmed rows; see fields/limit/offset.', {
@@ -403,7 +403,7 @@ server.tool('tasks_by_milestone', 'Get tasks for a milestone. Returns a page of
403
403
  ...taskFilterArgs,
404
404
  }, async (params) => {
405
405
  const result = await requireClient().getTasksByMilestone(params.milestone_id);
406
- const rows = Array.isArray(result) ? filterTasks(result, params) : result;
406
+ const rows = Array.isArray(result) ? annotateTasks(filterTasks(result, params)) : result;
407
407
  return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
408
408
  });
409
409
  server.tool('tasks_by_group_user', 'Get tasks by project group and user. Returns a page of trimmed rows; see fields/limit/offset.', {
@@ -413,7 +413,7 @@ server.tool('tasks_by_group_user', 'Get tasks by project group and user. Returns
413
413
  ...taskFilterArgs,
414
414
  }, async (params) => {
415
415
  const result = await requireClient().getTasksByGroupAndUser(params.group_id, params.user_id);
416
- const rows = Array.isArray(result) ? filterTasks(result, params) : result;
416
+ const rows = Array.isArray(result) ? annotateTasks(filterTasks(result, params)) : result;
417
417
  return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
418
418
  });
419
419
  server.tool('tasks_by_company', 'Get tasks for a company. Returns a page of trimmed rows; see fields/limit/offset.', {
@@ -423,7 +423,7 @@ server.tool('tasks_by_company', 'Get tasks for a company. Returns a page of trim
423
423
  }, async (params) => {
424
424
  const result = await requireClient().getCompanyTasks(params.company_id);
425
425
  const rows = Array.isArray(result)
426
- ? filterTasks(result, { ...params, company_id: undefined })
426
+ ? annotateTasks(filterTasks(result, { ...params, company_id: undefined }))
427
427
  : result;
428
428
  return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
429
429
  });
@@ -8,7 +8,16 @@
8
8
  * read. These helpers trim to a PM-relevant default, page the result, and tell
9
9
  * the caller what was left out so it can ask for more deliberately.
10
10
  */
11
- /** Task fields worth returning by default, of roughly 90 available. */
11
+ /**
12
+ * Task fields worth returning by default, of roughly 90 available.
13
+ *
14
+ * `deadline` is deliberately excluded. It is a server-rendered string that does
15
+ * not track the task's own due date: measured against six live tasks it read
16
+ * "In 2 Days" for one 39 days overdue, and gave "In 4 Days" for tasks at both
17
+ * -9 and +2 days. `end` is the real due date, and `pastdue` agrees with it on
18
+ * every row, so annotateTasks() derives due_date / due_in_days / overdue from
19
+ * `end` and those are what callers should read.
20
+ */
12
21
  export declare const TASK_FIELDS: string[];
13
22
  /** Project (group) fields worth returning by default, of 87 available. */
14
23
  export declare const PROJECT_FIELDS: string[];
@@ -38,6 +47,17 @@ export interface TaskFilters {
38
47
  }
39
48
  /** Apply the filters the backend does not support, in-process. */
40
49
  export declare function filterTasks(rows: unknown[], f: TaskFilters): unknown[];
50
+ /**
51
+ * Add an unambiguous due-date signal to task rows.
52
+ *
53
+ * The API ships three overlapping fields: `end` (the real due date), `pastdue`
54
+ * (a boolean that matches it), and `deadline` (a human string that does not).
55
+ * Rather than make every caller know which to trust, derive the answer from
56
+ * `end` and pass `deadline` through only when explicitly requested.
57
+ *
58
+ * due_in_days is negative for overdue work, 0 for due today.
59
+ */
60
+ export declare function annotateTasks(rows: unknown[], now?: number): unknown[];
41
61
  /**
42
62
  * Trim, page, and annotate a list response. Non-array payloads (a single record,
43
63
  * or an error object) pass through untouched.
@@ -8,12 +8,22 @@
8
8
  * read. These helpers trim to a PM-relevant default, page the result, and tell
9
9
  * the caller what was left out so it can ask for more deliberately.
10
10
  */
11
- /** Task fields worth returning by default, of roughly 90 available. */
11
+ /**
12
+ * Task fields worth returning by default, of roughly 90 available.
13
+ *
14
+ * `deadline` is deliberately excluded. It is a server-rendered string that does
15
+ * not track the task's own due date: measured against six live tasks it read
16
+ * "In 2 Days" for one 39 days overdue, and gave "In 4 Days" for tasks at both
17
+ * -9 and +2 days. `end` is the real due date, and `pastdue` agrees with it on
18
+ * every row, so annotateTasks() derives due_date / due_in_days / overdue from
19
+ * `end` and those are what callers should read.
20
+ */
12
21
  export const TASK_FIELDS = [
13
22
  'task_id', 'task', 'status', 'percentage', 'priority',
14
23
  'company_id', 'company_name', 'group_id', 'group_name',
15
24
  'milestone_id', 'milestone', 'estimated_time', 'total_duration_str',
16
- 'deadline', 'start', 'end', 'staff_assignees', 'total_comments', 'unread_for_me',
25
+ 'start', 'due_date', 'due_in_days', 'overdue', 'pastdue',
26
+ 'staff_assignees', 'total_comments', 'unread_for_me',
17
27
  ];
18
28
  /** Project (group) fields worth returning by default, of 87 available. */
19
29
  export const PROJECT_FIELDS = [
@@ -92,6 +102,32 @@ export function filterTasks(rows, f) {
92
102
  return true;
93
103
  });
94
104
  }
105
+ /**
106
+ * Add an unambiguous due-date signal to task rows.
107
+ *
108
+ * The API ships three overlapping fields: `end` (the real due date), `pastdue`
109
+ * (a boolean that matches it), and `deadline` (a human string that does not).
110
+ * Rather than make every caller know which to trust, derive the answer from
111
+ * `end` and pass `deadline` through only when explicitly requested.
112
+ *
113
+ * due_in_days is negative for overdue work, 0 for due today.
114
+ */
115
+ export function annotateTasks(rows, now = Date.now()) {
116
+ return rows.map((row) => {
117
+ if (!isRecord(row))
118
+ return row;
119
+ const due = Date.parse(String(row.end ?? ''));
120
+ if (!Number.isFinite(due)) {
121
+ return { ...row, due_date: null, due_in_days: null, overdue: null };
122
+ }
123
+ return {
124
+ ...row,
125
+ due_date: new Date(due).toISOString(),
126
+ due_in_days: Math.round((due - now) / 86_400_000),
127
+ overdue: due < now,
128
+ };
129
+ });
130
+ }
95
131
  function pick(row, fields) {
96
132
  if (!isRecord(row))
97
133
  return row;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viviscape-mcp",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "MCP server for the ViviScape API — CRM, projects, companies, notes, and insights, authenticated as the signed-in ViviScape user",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",