viviscape-mcp 2.8.0 → 2.9.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 +257 -253
- package/dist/api-client.d.ts +52 -0
- package/dist/api-client.js +137 -0
- package/dist/auth/permissions.js +10 -0
- package/dist/index.js +80 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,200 +1,203 @@
|
|
|
1
|
-
# viviscape-mcp
|
|
2
|
-
|
|
3
|
-
MCP server for the [ViviScape](https://viviscape.io) API - exposes CRM, projects, companies, tasks, time tracking, notes, and insights as tools for Claude and other MCP clients.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
No install needed - run via `npx`:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npx viviscape-mcp
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Or install globally:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install -g viviscape-mcp
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Sign in
|
|
20
|
-
|
|
21
|
-
Authentication is per-user, the same browser flow the ViviScape CLI uses - no
|
|
22
|
-
API keys. Sign in once:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npx viviscape-mcp login
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
A browser tab opens on ViviScape Work; confirm **Allow access** and the session
|
|
29
|
-
is stored in Windows Credential Manager (or `~/.viviscape/mcp-credentials.json`
|
|
30
|
-
on macOS/Linux). Every tool call then runs as *you*: your user id, account,
|
|
31
|
-
role, and plan come from the session, so tools see exactly the data you can see
|
|
32
|
-
in the app.
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx viviscape-mcp status # who am I, which account, role, plan
|
|
36
|
-
npx viviscape-mcp logout # clear the stored session
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
If you already ran `vs auth login` with the [ViviScape CLI](https://viviscape.io),
|
|
40
|
-
that session is picked up automatically. Inside an MCP client you can also call
|
|
41
|
-
the `auth_login`, `auth_status`, and `auth_logout` tools. Sessions expire; when
|
|
42
|
-
one does, tools report it and you re-run `login`.
|
|
43
|
-
|
|
44
|
-
## Configuration
|
|
45
|
-
|
|
46
|
-
| Variable | Required | Default | Description |
|
|
47
|
-
|----------|----------|---------|-------------|
|
|
48
|
-
| `VIVISCAPE_BASE_URL` | no | `https://work.viviscape.io` | ViviScape Work base URL |
|
|
49
|
-
|
|
50
|
-
## Claude Desktop / Claude Code
|
|
51
|
-
|
|
52
|
-
Add to your MCP client config (e.g. `claude_desktop_config.json` or `.mcp.json`):
|
|
53
|
-
|
|
54
|
-
```json
|
|
55
|
-
{
|
|
56
|
-
"mcpServers": {
|
|
57
|
-
"viviscape": {
|
|
58
|
-
"command": "npx",
|
|
59
|
-
"args": ["-y", "viviscape-mcp"]
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## OpenAI Codex CLI
|
|
66
|
-
|
|
67
|
-
Codex accepts stdio MCP servers directly. Add to `~/.codex/config.toml`:
|
|
68
|
-
|
|
69
|
-
```toml
|
|
70
|
-
[mcp_servers.viviscape]
|
|
71
|
-
command = "npx"
|
|
72
|
-
args = ["-y", "viviscape-mcp"]
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## ChatGPT (Developer Mode / Apps SDK)
|
|
76
|
-
|
|
77
|
-
ChatGPT only accepts **remote HTTP** MCP servers - it does not run local stdio
|
|
78
|
-
processes. Bridge this server to an HTTP endpoint with
|
|
79
|
-
[`mcp-remote`](https://www.npmjs.com/package/mcp-remote) or
|
|
80
|
-
[`supergateway`](https://www.npmjs.com/package/supergateway), then expose it
|
|
81
|
-
publicly (Cloudflare Tunnel, ngrok, or deploy to a host).
|
|
82
|
-
|
|
83
|
-
Example with `supergateway`:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
npx viviscape-mcp login # once, on the host running the gateway
|
|
87
|
-
npx -y supergateway --stdio "npx -y viviscape-mcp" --port 8000
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
The gateway shares the host's stored session, so everyone reaching that endpoint
|
|
91
|
-
acts as the signed-in user - put your own auth in front of it.
|
|
92
|
-
|
|
93
|
-
Expose `http://localhost:8000/sse` publicly, then in ChatGPT:
|
|
94
|
-
|
|
95
|
-
**Settings -> Connectors -> Create -> Custom MCP server**
|
|
96
|
-
- **URL:** `https://your-public-host/sse`
|
|
97
|
-
- **Auth:** none (or bearer, depending on your gateway)
|
|
98
|
-
|
|
99
|
-
## Tools
|
|
100
|
-
|
|
101
|
-
The server exposes tools across these domains:
|
|
102
|
-
|
|
103
|
-
- **Prospects** - `prospect_add`, `prospect_get`, `prospect_update`, `prospect_query`, follow-ups, notes
|
|
104
|
-
- **Clients** - `client_add`, `client_get`, `client_get_by_email`, `clients_by_company`
|
|
105
|
-
- **Companies** - `company_add`, `company_list`, `company_update`
|
|
106
|
-
- **Projects** - `project_get`, `project_list`, `project_list_active`, `project_staff`, `project_tasks`, `projects_by_company`
|
|
107
|
-
- **Tasks** - `task_add`, `task_get`, `task_update`, `tasks_open`, `tasks_pending`, `tasks_by_company`, `tasks_by_milestone`
|
|
108
|
-
- **Task comments** - `task_comments`, `task_comment_get`, `task_comment_add`, `task_comment_update`, `task_comment_remove`, `task_mark_read`
|
|
109
|
-
- **Task assignment** - `task_assignee_add`, `task_assignee_remove`, `task_set_leader`, `task_set_attention`, `task_delete`, `task_merge`
|
|
110
|
-
- **Task tags** - `task_tags` (list/add/remove), `tasks_by_tag` (tasks carrying a label, or the account tag vocabulary), and a `tags` list on `task_add`
|
|
111
|
-
- **Task references** - `task_references` (list/add/remove), `task_search` (find a task by keyword or id)
|
|
112
|
-
- **Milestones** - `milestone_list`, `milestones_active`, `milestones_by_company`, `milestones_by_user`, `milestone_get`, `milestone_add`, `milestone_update`, `milestone_clone`, `milestone_remove`
|
|
113
|
-
- **Project writes** - `project_add`, `project_update`, `project_set_status`, `project_user_add`, `project_user_remove`
|
|
114
|
-
- **Reference** - `enums` (the status, priority, and group-type values the platform accepts)
|
|
115
|
-
- **Lookup** - `services` (resolve service_id to a name), `user_lookup` (id, email, team, groups)
|
|
116
|
-
- **Bulk** - `tasks_bulk_add` (many tasks in one request)
|
|
117
|
-
- **Time logs** - `timelog_add`, `timelog_update`
|
|
118
|
-
- **
|
|
1
|
+
# viviscape-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for the [ViviScape](https://viviscape.io) API - exposes CRM, projects, companies, tasks, time tracking, notes, and insights as tools for Claude and other MCP clients.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
No install needed - run via `npx`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx viviscape-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install globally:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g viviscape-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Sign in
|
|
20
|
+
|
|
21
|
+
Authentication is per-user, the same browser flow the ViviScape CLI uses - no
|
|
22
|
+
API keys. Sign in once:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx viviscape-mcp login
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A browser tab opens on ViviScape Work; confirm **Allow access** and the session
|
|
29
|
+
is stored in Windows Credential Manager (or `~/.viviscape/mcp-credentials.json`
|
|
30
|
+
on macOS/Linux). Every tool call then runs as *you*: your user id, account,
|
|
31
|
+
role, and plan come from the session, so tools see exactly the data you can see
|
|
32
|
+
in the app.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx viviscape-mcp status # who am I, which account, role, plan
|
|
36
|
+
npx viviscape-mcp logout # clear the stored session
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If you already ran `vs auth login` with the [ViviScape CLI](https://viviscape.io),
|
|
40
|
+
that session is picked up automatically. Inside an MCP client you can also call
|
|
41
|
+
the `auth_login`, `auth_status`, and `auth_logout` tools. Sessions expire; when
|
|
42
|
+
one does, tools report it and you re-run `login`.
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
| Variable | Required | Default | Description |
|
|
47
|
+
|----------|----------|---------|-------------|
|
|
48
|
+
| `VIVISCAPE_BASE_URL` | no | `https://work.viviscape.io` | ViviScape Work base URL |
|
|
49
|
+
|
|
50
|
+
## Claude Desktop / Claude Code
|
|
51
|
+
|
|
52
|
+
Add to your MCP client config (e.g. `claude_desktop_config.json` or `.mcp.json`):
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"viviscape": {
|
|
58
|
+
"command": "npx",
|
|
59
|
+
"args": ["-y", "viviscape-mcp"]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## OpenAI Codex CLI
|
|
66
|
+
|
|
67
|
+
Codex accepts stdio MCP servers directly. Add to `~/.codex/config.toml`:
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[mcp_servers.viviscape]
|
|
71
|
+
command = "npx"
|
|
72
|
+
args = ["-y", "viviscape-mcp"]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## ChatGPT (Developer Mode / Apps SDK)
|
|
76
|
+
|
|
77
|
+
ChatGPT only accepts **remote HTTP** MCP servers - it does not run local stdio
|
|
78
|
+
processes. Bridge this server to an HTTP endpoint with
|
|
79
|
+
[`mcp-remote`](https://www.npmjs.com/package/mcp-remote) or
|
|
80
|
+
[`supergateway`](https://www.npmjs.com/package/supergateway), then expose it
|
|
81
|
+
publicly (Cloudflare Tunnel, ngrok, or deploy to a host).
|
|
82
|
+
|
|
83
|
+
Example with `supergateway`:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx viviscape-mcp login # once, on the host running the gateway
|
|
87
|
+
npx -y supergateway --stdio "npx -y viviscape-mcp" --port 8000
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The gateway shares the host's stored session, so everyone reaching that endpoint
|
|
91
|
+
acts as the signed-in user - put your own auth in front of it.
|
|
92
|
+
|
|
93
|
+
Expose `http://localhost:8000/sse` publicly, then in ChatGPT:
|
|
94
|
+
|
|
95
|
+
**Settings -> Connectors -> Create -> Custom MCP server**
|
|
96
|
+
- **URL:** `https://your-public-host/sse`
|
|
97
|
+
- **Auth:** none (or bearer, depending on your gateway)
|
|
98
|
+
|
|
99
|
+
## Tools
|
|
100
|
+
|
|
101
|
+
The server exposes tools across these domains:
|
|
102
|
+
|
|
103
|
+
- **Prospects** - `prospect_add`, `prospect_get`, `prospect_update`, `prospect_query`, follow-ups, notes
|
|
104
|
+
- **Clients** - `client_add`, `client_get`, `client_get_by_email`, `clients_by_company`
|
|
105
|
+
- **Companies** - `company_add`, `company_list`, `company_update`
|
|
106
|
+
- **Projects** - `project_get`, `project_list`, `project_list_active`, `project_staff`, `project_tasks`, `projects_by_company`
|
|
107
|
+
- **Tasks** - `task_add`, `task_get`, `task_update`, `tasks_open`, `tasks_pending`, `tasks_by_company`, `tasks_by_milestone`
|
|
108
|
+
- **Task comments** - `task_comments`, `task_comment_get`, `task_comment_add`, `task_comment_update`, `task_comment_remove`, `task_mark_read`
|
|
109
|
+
- **Task assignment** - `task_assignee_add`, `task_assignee_remove`, `task_set_leader`, `task_set_attention`, `task_delete`, `task_merge`, `task_complete`
|
|
110
|
+
- **Task tags** - `task_tags` (list/add/remove), `tasks_by_tag` (tasks carrying a label, or the account tag vocabulary), and a `tags` list on `task_add`
|
|
111
|
+
- **Task references** - `task_references` (list/add/remove), `task_search` (find a task by keyword or id)
|
|
112
|
+
- **Milestones** - `milestone_list`, `milestones_active`, `milestones_by_company`, `milestones_by_user`, `milestone_get`, `milestone_add`, `milestone_update`, `milestone_clone`, `milestone_remove`
|
|
113
|
+
- **Project writes** - `project_add`, `project_update`, `project_set_status`, `project_user_add`, `project_user_remove`
|
|
114
|
+
- **Reference** - `enums` (the status, priority, and group-type values the platform accepts)
|
|
115
|
+
- **Lookup** - `services` (resolve service_id to a name), `user_lookup` (id, email, team, groups)
|
|
116
|
+
- **Bulk** - `tasks_bulk_add` (many tasks in one request)
|
|
117
|
+
- **Time logs** - `timelog_add`, `timelog_update`, `task_log_time` (quick log against a task, or a note-only entry)
|
|
118
|
+
- **Personal logs** - `personal_logs`, `personal_log_add`, `personal_log_remove` (service work that belongs to no task)
|
|
119
|
+
- **Time clock** - `timeclock_punch` (in/out), `timeclock_history`
|
|
120
|
+
- **Tickets** - `tickets_list` (the open, closed, or complete ticket queue; enterprise plan)
|
|
121
|
+
- **Notes** - `note_add`, `note_get`, `note_update`, `note_remove`, `notes_mine`, `notes_query`, `notes_account`, `note_revisions`
|
|
119
122
|
- **Second Brain** - `kb_search` (semantic search over tickets, plans, notes, products, resolutions and SOPs), `kb_sop` (list/get/save an SOP), `kb_status`, `kb_reindex`
|
|
120
|
-
- **Note filing** - `note_companies`, `note_users`, `note_tags`, `note_attachments`, `notebook`, `notebook_users` (each takes an `action`)
|
|
121
|
-
- **Files** - `project_files` (list/upload/update/delete/download), `task_files`
|
|
122
|
-
- **Insights** - hours by person/service/project, AI summary, person stats, time totals
|
|
123
|
-
- **Account** - `account_info`, `account_services`, `account_users`
|
|
124
|
-
- **Auth** - `auth_login`, `auth_status`, `auth_logout`
|
|
125
|
-
|
|
126
|
-
### Status and priority values
|
|
127
|
-
|
|
128
|
-
Use the `enums` tool rather than guessing. The platform's vocabularies are:
|
|
129
|
-
|
|
130
|
-
| field | values |
|
|
131
|
-
|---|---|
|
|
132
|
-
| task status | backlog, new, research, discussion, inprogress, pendingreview, inreview, testing, waitingcustomer, waitingteammember, onhold, cancelled, completed |
|
|
133
|
-
| priority | low, moderate, important, urgent, critical |
|
|
134
|
-
| project status | new, inprogress, complete |
|
|
135
|
-
| prospect status | new, firstcontact, negotiation, pending, won, lost, spam |
|
|
136
|
-
|
|
137
|
-
Two traps: tasks complete as `completed` while projects complete as
|
|
138
|
-
`complete`, and priority is *not* low/medium/high. Task and project tools
|
|
139
|
-
validate these with `z.enum`, so an invalid value is rejected before it reaches
|
|
140
|
-
the API.
|
|
141
|
-
|
|
142
|
-
### Task tags
|
|
143
|
-
|
|
144
|
-
`task_tags` lists, adds, and removes the tags shown on the task board, and
|
|
145
|
-
`task_add` takes a `tags` list so a generated task lands already categorised
|
|
146
|
-
(handy for marking agent-written work).
|
|
147
|
-
|
|
148
|
-
Verified behaviour, so callers do not have to discover it:
|
|
149
|
-
|
|
150
|
-
- Labels are stored upper-case, and `add` is idempotent per label - re-adding
|
|
151
|
-
one returns the existing tag, not a duplicate.
|
|
152
|
-
- `add` returns the persisted tag with its `tag_id`; `remove` takes that
|
|
153
|
-
`tag_id`, not the label, and answers `true` (unlike `note_tags`, whose remove
|
|
154
|
-
answers `false` even on success).
|
|
155
|
-
- `color` is any hex string, defaulting to the board palette's first entry
|
|
156
|
-
(`#00325e`).
|
|
157
|
-
- `task_tags` is per task, and list rows from `tasks_open` / `project_tasks`
|
|
158
|
-
carry `tags: null`, so read one task's tags with `task_tags` or `task_get`.
|
|
159
|
-
- `tasks_by_tag` goes the other way, from a label to the tasks carrying it.
|
|
160
|
-
Labels match trimmed and case insensitively; `match: "any"` (default) returns
|
|
161
|
-
a task carrying at least one label, `match: "all"` only tasks carrying every
|
|
162
|
-
one. An optional `status` list narrows further. Hits are light rows that
|
|
163
|
-
carry the task's whole tag set, so the other labels arrive with the hit.
|
|
164
|
-
Called with no `tags`, it returns the account's tag vocabulary instead -
|
|
165
|
-
every distinct label with the number of tasks using it.
|
|
166
|
-
- A tag failure inside `task_add` never fails the create: the task is returned
|
|
167
|
-
with the per-tag error recorded in its `tags` array.
|
|
168
|
-
|
|
169
|
-
### Task references
|
|
170
|
-
|
|
171
|
-
`task_references` links related tasks to each other - a duplicate, a blocker,
|
|
172
|
-
the ticket a task came from - and `task_search` finds the id to link when only
|
|
173
|
-
a keyword is known.
|
|
174
|
-
|
|
175
|
-
Verified behaviour:
|
|
176
|
-
|
|
177
|
-
- Links are **symmetric**. Adding A to B makes the pair visible from both
|
|
178
|
-
tasks, so link once; mirroring it by hand just returns the same row.
|
|
179
|
-
- `list` rows describe the *other* task: `related_task_id` plus its title,
|
|
180
|
-
status, project and company, so a reference list needs no follow-up
|
|
181
|
-
`task_get`.
|
|
182
|
-
- `add` is idempotent per pair (in either direction) and returns the persisted
|
|
183
|
-
link with its `reference_id`; `remove` takes that `reference_id`, not a task
|
|
184
|
-
id, and answers `true`.
|
|
185
|
-
- Both tasks must belong to the signed-in account; a cross-account id comes
|
|
186
|
-
back as an empty link rather than an error.
|
|
187
|
-
- `task_search` matches the task title, the description, and - for a numeric
|
|
188
|
-
query - the task id itself. It covers tickets too, so it is the one keyword
|
|
189
|
-
route that reaches ticket rows `tasks_open` never returns.
|
|
190
|
-
|
|
191
|
-
### Task notes
|
|
192
|
-
|
|
193
|
-
`task_update` takes a `notes` field: the free-form working notes on the task's
|
|
194
|
-
Overview tab, stored as HTML by the rich-text editor. It **replaces** the field
|
|
195
|
-
outright, so read the current value with `task_get` and send the merged text
|
|
196
|
-
rather than only the new lines.
|
|
197
|
-
|
|
123
|
+
- **Note filing** - `note_companies`, `note_users`, `note_tags`, `note_attachments`, `notebook`, `notebook_users` (each takes an `action`)
|
|
124
|
+
- **Files** - `project_files` (list/upload/update/delete/download), `task_files`
|
|
125
|
+
- **Insights** - hours by person/service/project, AI summary, person stats, time totals
|
|
126
|
+
- **Account** - `account_info`, `account_services`, `account_users`
|
|
127
|
+
- **Auth** - `auth_login`, `auth_status`, `auth_logout`
|
|
128
|
+
|
|
129
|
+
### Status and priority values
|
|
130
|
+
|
|
131
|
+
Use the `enums` tool rather than guessing. The platform's vocabularies are:
|
|
132
|
+
|
|
133
|
+
| field | values |
|
|
134
|
+
|---|---|
|
|
135
|
+
| task status | backlog, new, research, discussion, inprogress, pendingreview, inreview, testing, waitingcustomer, waitingteammember, onhold, cancelled, completed |
|
|
136
|
+
| priority | low, moderate, important, urgent, critical |
|
|
137
|
+
| project status | new, inprogress, complete |
|
|
138
|
+
| prospect status | new, firstcontact, negotiation, pending, won, lost, spam |
|
|
139
|
+
|
|
140
|
+
Two traps: tasks complete as `completed` while projects complete as
|
|
141
|
+
`complete`, and priority is *not* low/medium/high. Task and project tools
|
|
142
|
+
validate these with `z.enum`, so an invalid value is rejected before it reaches
|
|
143
|
+
the API.
|
|
144
|
+
|
|
145
|
+
### Task tags
|
|
146
|
+
|
|
147
|
+
`task_tags` lists, adds, and removes the tags shown on the task board, and
|
|
148
|
+
`task_add` takes a `tags` list so a generated task lands already categorised
|
|
149
|
+
(handy for marking agent-written work).
|
|
150
|
+
|
|
151
|
+
Verified behaviour, so callers do not have to discover it:
|
|
152
|
+
|
|
153
|
+
- Labels are stored upper-case, and `add` is idempotent per label - re-adding
|
|
154
|
+
one returns the existing tag, not a duplicate.
|
|
155
|
+
- `add` returns the persisted tag with its `tag_id`; `remove` takes that
|
|
156
|
+
`tag_id`, not the label, and answers `true` (unlike `note_tags`, whose remove
|
|
157
|
+
answers `false` even on success).
|
|
158
|
+
- `color` is any hex string, defaulting to the board palette's first entry
|
|
159
|
+
(`#00325e`).
|
|
160
|
+
- `task_tags` is per task, and list rows from `tasks_open` / `project_tasks`
|
|
161
|
+
carry `tags: null`, so read one task's tags with `task_tags` or `task_get`.
|
|
162
|
+
- `tasks_by_tag` goes the other way, from a label to the tasks carrying it.
|
|
163
|
+
Labels match trimmed and case insensitively; `match: "any"` (default) returns
|
|
164
|
+
a task carrying at least one label, `match: "all"` only tasks carrying every
|
|
165
|
+
one. An optional `status` list narrows further. Hits are light rows that
|
|
166
|
+
carry the task's whole tag set, so the other labels arrive with the hit.
|
|
167
|
+
Called with no `tags`, it returns the account's tag vocabulary instead -
|
|
168
|
+
every distinct label with the number of tasks using it.
|
|
169
|
+
- A tag failure inside `task_add` never fails the create: the task is returned
|
|
170
|
+
with the per-tag error recorded in its `tags` array.
|
|
171
|
+
|
|
172
|
+
### Task references
|
|
173
|
+
|
|
174
|
+
`task_references` links related tasks to each other - a duplicate, a blocker,
|
|
175
|
+
the ticket a task came from - and `task_search` finds the id to link when only
|
|
176
|
+
a keyword is known.
|
|
177
|
+
|
|
178
|
+
Verified behaviour:
|
|
179
|
+
|
|
180
|
+
- Links are **symmetric**. Adding A to B makes the pair visible from both
|
|
181
|
+
tasks, so link once; mirroring it by hand just returns the same row.
|
|
182
|
+
- `list` rows describe the *other* task: `related_task_id` plus its title,
|
|
183
|
+
status, project and company, so a reference list needs no follow-up
|
|
184
|
+
`task_get`.
|
|
185
|
+
- `add` is idempotent per pair (in either direction) and returns the persisted
|
|
186
|
+
link with its `reference_id`; `remove` takes that `reference_id`, not a task
|
|
187
|
+
id, and answers `true`.
|
|
188
|
+
- Both tasks must belong to the signed-in account; a cross-account id comes
|
|
189
|
+
back as an empty link rather than an error.
|
|
190
|
+
- `task_search` matches the task title, the description, and - for a numeric
|
|
191
|
+
query - the task id itself. It covers tickets too, so it is the one keyword
|
|
192
|
+
route that reaches ticket rows `tasks_open` never returns.
|
|
193
|
+
|
|
194
|
+
### Task notes
|
|
195
|
+
|
|
196
|
+
`task_update` takes a `notes` field: the free-form working notes on the task's
|
|
197
|
+
Overview tab, stored as HTML by the rich-text editor. It **replaces** the field
|
|
198
|
+
outright, so read the current value with `task_get` and send the merged text
|
|
199
|
+
rather than only the new lines.
|
|
200
|
+
|
|
198
201
|
### Task plans
|
|
199
202
|
|
|
200
203
|
`task_add` and `task_update` take a `plan` field: the plan of action for the
|
|
@@ -231,60 +234,61 @@ knowledge an agent authors directly, so `kb_sop` save is how a lesson from one
|
|
|
231
234
|
ticket becomes reusable — search `source_types: ["sop"]` first and update the
|
|
232
235
|
existing SOP rather than filing a near-duplicate.
|
|
233
236
|
|
|
234
|
-
### Repeat-safe creates
|
|
235
|
-
|
|
236
|
-
No create route accepts an idempotency key, so a retried agent step silently
|
|
237
|
-
creates a second record. `task_add`, `note_add`, `timelog_add`,
|
|
238
|
-
`
|
|
239
|
-
`
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
- `
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
`
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
`
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
npm
|
|
284
|
-
npm run
|
|
285
|
-
npm
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
237
|
+
### Repeat-safe creates
|
|
238
|
+
|
|
239
|
+
No create route accepts an idempotency key, so a retried agent step silently
|
|
240
|
+
creates a second record. `task_add`, `note_add`, `timelog_add`,
|
|
241
|
+
`task_log_time`, `personal_log_add`, `timeclock_punch`, `task_complete`,
|
|
242
|
+
`prospect_add`, and `tasks_bulk_add` accept an optional
|
|
243
|
+
`idempotency_key`: calling again with the same key replays the first result
|
|
244
|
+
instead of writing again.
|
|
245
|
+
|
|
246
|
+
The record is local (`~/.viviscape/mcp-idempotency.json`, 7-day expiry), so it
|
|
247
|
+
stops the common case -- the same agent retrying the same step -- and does not
|
|
248
|
+
make the server idempotent. Two machines running the same plan will still
|
|
249
|
+
duplicate.
|
|
250
|
+
|
|
251
|
+
### Delta queries
|
|
252
|
+
|
|
253
|
+
List tools accept `updated_since` (ISO 8601) and return only rows touched at
|
|
254
|
+
or after that time, so a recurring agent does not re-read the whole working set.
|
|
255
|
+
It is filtered in-process (no list route supports a modified-since filter), and
|
|
256
|
+
rows carrying no usable timestamp are kept rather than dropped.
|
|
257
|
+
|
|
258
|
+
### Paging and field selection
|
|
259
|
+
|
|
260
|
+
List tools (`tasks_open`, `project_list_active`, `project_tasks`, and the rest)
|
|
261
|
+
return a page of trimmed rows rather than every column of every row. A task row
|
|
262
|
+
carries ~94 columns and a project row 87, most of them irrelevant to project
|
|
263
|
+
work, so each list tool accepts:
|
|
264
|
+
|
|
265
|
+
- `fields` - columns to return; omit for a curated default, or pass `["all"]`
|
|
266
|
+
- `limit` / `offset` - page window, default 50 rows
|
|
267
|
+
|
|
268
|
+
Task rows also carry computed `due_date`, `due_in_days` (negative when
|
|
269
|
+
overdue), and `overdue`, derived from the task's `end` date. Read those rather
|
|
270
|
+
than the API's `deadline` string, which does not track the due date -- it
|
|
271
|
+
reported "In 2 Days" for a task 39 days overdue. `deadline` is excluded from the
|
|
272
|
+
default field set; ask for it explicitly if you need to see what the UI shows.
|
|
273
|
+
|
|
274
|
+
Responses are wrapped as `{ total, returned, offset, next_offset, fields, items }`
|
|
275
|
+
so a caller can tell when more rows exist. Task lists also accept
|
|
276
|
+
`company_id`, `project_id`, `assignee_id`, `status`, `priority`,
|
|
277
|
+
`due_before`, `due_after`, and `search`; `tasks_open` pushes `company_id`
|
|
278
|
+
and `only_mine` to the server and filters the rest in-process.
|
|
279
|
+
|
|
280
|
+
In practice this took `tasks_open` from 97 KB to 6.4 KB and
|
|
281
|
+
`project_list_active` from 375 KB to 19 KB.
|
|
282
|
+
|
|
283
|
+
## Development
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
npm install
|
|
287
|
+
npm run dev # run with tsx, hot reload
|
|
288
|
+
npm run build # compile to dist/
|
|
289
|
+
npm start # run compiled
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT
|
package/dist/api-client.d.ts
CHANGED
|
@@ -153,6 +153,25 @@ export declare class ViviScapeClient {
|
|
|
153
153
|
deleteTask(taskId: number): Promise<unknown>;
|
|
154
154
|
/** Fold one task into another; the source task is consumed. */
|
|
155
155
|
mergeTasks(fromTaskId: number, toTaskId: number): Promise<unknown>;
|
|
156
|
+
/**
|
|
157
|
+
* Close a task through the platform's completion route (what `vs tasks done`
|
|
158
|
+
* calls). This is not the same as posting status "completed": the route also
|
|
159
|
+
* stamps the completing user and runs the milestone/progress rollup the task
|
|
160
|
+
* board relies on.
|
|
161
|
+
*/
|
|
162
|
+
completeTask(taskId: number, userId?: number): Promise<unknown>;
|
|
163
|
+
/**
|
|
164
|
+
* The ticket queue, filtered server-side by status set, company and team --
|
|
165
|
+
* the view behind `vs tickets list`. Closed tickets live on their own route.
|
|
166
|
+
* Tickets are Group_Tasks, so the rows are task rows and task_get reads one.
|
|
167
|
+
*/
|
|
168
|
+
lookupTickets(opts?: {
|
|
169
|
+
status?: 'open' | 'closed' | 'all';
|
|
170
|
+
statuses?: string[];
|
|
171
|
+
query?: string;
|
|
172
|
+
company_id?: number;
|
|
173
|
+
team?: string;
|
|
174
|
+
}): Promise<unknown>;
|
|
156
175
|
getTaskTags(taskId: number): Promise<unknown>;
|
|
157
176
|
/**
|
|
158
177
|
* Tag a task. The web app upper-cases the label and always sends a colour, so
|
|
@@ -209,6 +228,39 @@ export declare class ViviScapeClient {
|
|
|
209
228
|
removeMilestone(milestoneId: number): Promise<unknown>;
|
|
210
229
|
addTimeLog(data: Record<string, unknown>): Promise<unknown>;
|
|
211
230
|
updateTimeLog(data: Record<string, unknown>): Promise<unknown>;
|
|
231
|
+
/**
|
|
232
|
+
* Log time against a task the short way (`vs tasks log`). The quick route
|
|
233
|
+
* fills group_id and service_id from the task itself, so only the note, the
|
|
234
|
+
* duration and the date are needed. A zero duration posts a note-only entry:
|
|
235
|
+
* it lands on the task's time feed without adding hours.
|
|
236
|
+
*/
|
|
237
|
+
quickLogTime(data: {
|
|
238
|
+
task_id: number;
|
|
239
|
+
note: string;
|
|
240
|
+
duration?: string;
|
|
241
|
+
log_date?: string;
|
|
242
|
+
billable?: boolean;
|
|
243
|
+
}): Promise<unknown>;
|
|
244
|
+
/** Personal (non-task) time logs for a user -- the `vs logs list` feed. */
|
|
245
|
+
getPersonalLogs(userId?: number): Promise<unknown>;
|
|
246
|
+
/**
|
|
247
|
+
* Add a personal time log: work that belongs to a service rather than to any
|
|
248
|
+
* task. start_date and end_date mirror log_date, which is what the web form
|
|
249
|
+
* posts for a single-day entry.
|
|
250
|
+
*/
|
|
251
|
+
addPersonalLog(data: {
|
|
252
|
+
note: string;
|
|
253
|
+
duration?: string;
|
|
254
|
+
service_id?: number;
|
|
255
|
+
log_date?: string;
|
|
256
|
+
}): Promise<unknown>;
|
|
257
|
+
removePersonalLog(logId: number | string): Promise<unknown>;
|
|
258
|
+
/**
|
|
259
|
+
* Punch the time clock. type "in" opens a punch, "out" closes the open one;
|
|
260
|
+
* both go to the same route with clocked_in set accordingly.
|
|
261
|
+
*/
|
|
262
|
+
addPunch(type: 'in' | 'out', note?: string): Promise<unknown>;
|
|
263
|
+
getPunchHistory(userId?: number): Promise<unknown>;
|
|
212
264
|
getMyNotes(): Promise<unknown>;
|
|
213
265
|
getNoteById(noteId: number | string): Promise<unknown>;
|
|
214
266
|
addNote(data: Record<string, unknown>): Promise<unknown>;
|
package/dist/api-client.js
CHANGED
|
@@ -50,6 +50,46 @@ function parseShorthandDuration(input) {
|
|
|
50
50
|
const mm = String(minutes).padStart(2, '0');
|
|
51
51
|
return { duration: `${hh}:${mm}:00`, log_time: raw };
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Total minutes from shorthand ("1h30m", "45m", "2h", "90"). Used by the quick
|
|
55
|
+
* log and personal log routes, which want human text ("1h 30m") rather than the
|
|
56
|
+
* HH:MM:SS TimeSpan the full LOG_Time payload carries.
|
|
57
|
+
*/
|
|
58
|
+
function shorthandToMinutes(input) {
|
|
59
|
+
const raw = String(input ?? '').trim();
|
|
60
|
+
if (!raw)
|
|
61
|
+
return 0;
|
|
62
|
+
const clock = raw.match(/^(\d{1,2}):(\d{2})(?::\d{2})?$/);
|
|
63
|
+
if (clock)
|
|
64
|
+
return parseInt(clock[1], 10) * 60 + parseInt(clock[2], 10);
|
|
65
|
+
const hMatch = raw.match(/(\d+(?:\.\d+)?)\s*h/i);
|
|
66
|
+
const mMatch = raw.match(/(\d+)\s*m/i);
|
|
67
|
+
if (!hMatch && !mMatch) {
|
|
68
|
+
const bare = Number(raw);
|
|
69
|
+
return Number.isFinite(bare) ? Math.round(bare) : 0;
|
|
70
|
+
}
|
|
71
|
+
const hours = hMatch ? parseFloat(hMatch[1]) : 0;
|
|
72
|
+
const minutes = mMatch ? parseInt(mMatch[1], 10) : 0;
|
|
73
|
+
return Math.round(hours * 60) + minutes;
|
|
74
|
+
}
|
|
75
|
+
/** "1h 30m" / "45m" -- the log_time text the quick and personal log routes store. */
|
|
76
|
+
function friendlyDuration(totalMinutes) {
|
|
77
|
+
if (totalMinutes <= 0)
|
|
78
|
+
return '';
|
|
79
|
+
if (totalMinutes < 60)
|
|
80
|
+
return `${totalMinutes}m`;
|
|
81
|
+
const h = Math.floor(totalMinutes / 60);
|
|
82
|
+
const m = totalMinutes % 60;
|
|
83
|
+
return m ? `${h}h ${m}m` : `${h}h`;
|
|
84
|
+
}
|
|
85
|
+
/** MM/DD/YYYY -- the date format both log routes expect. */
|
|
86
|
+
function usDate(value) {
|
|
87
|
+
const d = value ? new Date(value) : new Date();
|
|
88
|
+
const use = Number.isNaN(d.getTime()) ? new Date() : d;
|
|
89
|
+
const mm = String(use.getMonth() + 1).padStart(2, '0');
|
|
90
|
+
const dd = String(use.getDate()).padStart(2, '0');
|
|
91
|
+
return `${mm}/${dd}/${use.getFullYear()}`;
|
|
92
|
+
}
|
|
53
93
|
/**
|
|
54
94
|
* Build a full Group_Task payload from simplified MCP tool params.
|
|
55
95
|
*/
|
|
@@ -637,6 +677,39 @@ export class ViviScapeClient {
|
|
|
637
677
|
async mergeTasks(fromTaskId, toTaskId) {
|
|
638
678
|
return this.post('tasks/merge', { task_from_id: fromTaskId, task_to_id: toTaskId });
|
|
639
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Close a task through the platform's completion route (what `vs tasks done`
|
|
682
|
+
* calls). This is not the same as posting status "completed": the route also
|
|
683
|
+
* stamps the completing user and runs the milestone/progress rollup the task
|
|
684
|
+
* board relies on.
|
|
685
|
+
*/
|
|
686
|
+
async completeTask(taskId, userId) {
|
|
687
|
+
return this.get(`complete/task?task_id=${taskId}&user_id=${userId || this.userId}`);
|
|
688
|
+
}
|
|
689
|
+
// -- Tickets ------------------------------------------------
|
|
690
|
+
/**
|
|
691
|
+
* The ticket queue, filtered server-side by status set, company and team --
|
|
692
|
+
* the view behind `vs tickets list`. Closed tickets live on their own route.
|
|
693
|
+
* Tickets are Group_Tasks, so the rows are task rows and task_get reads one.
|
|
694
|
+
*/
|
|
695
|
+
async lookupTickets(opts = {}) {
|
|
696
|
+
const state = opts.status ?? 'open';
|
|
697
|
+
const statuses = opts.statuses?.length
|
|
698
|
+
? opts.statuses
|
|
699
|
+
: state === 'all'
|
|
700
|
+
? []
|
|
701
|
+
: state === 'closed'
|
|
702
|
+
? ['completed', 'cancelled']
|
|
703
|
+
: ['backlog', 'new', 'research', 'inprogress', 'testing', 'inreview', 'onhold'];
|
|
704
|
+
const path = state === 'closed' ? 'tickets/completed/lookup' : 'tickets/lookup';
|
|
705
|
+
return this.post(path, {
|
|
706
|
+
company_id: opts.company_id ?? 0,
|
|
707
|
+
status: '',
|
|
708
|
+
statuses,
|
|
709
|
+
query: opts.query ?? '',
|
|
710
|
+
teamfilter: opts.team ?? '',
|
|
711
|
+
});
|
|
712
|
+
}
|
|
640
713
|
// -- Task tags ----------------------------------------------
|
|
641
714
|
async getTaskTags(taskId) {
|
|
642
715
|
return this.get(`grouptask/tags/${taskId}`);
|
|
@@ -781,6 +854,70 @@ export class ViviScapeClient {
|
|
|
781
854
|
async updateTimeLog(data) {
|
|
782
855
|
return this.post('logs/update', buildTimeLogPayload(data, this.ctx));
|
|
783
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Log time against a task the short way (`vs tasks log`). The quick route
|
|
859
|
+
* fills group_id and service_id from the task itself, so only the note, the
|
|
860
|
+
* duration and the date are needed. A zero duration posts a note-only entry:
|
|
861
|
+
* it lands on the task's time feed without adding hours.
|
|
862
|
+
*/
|
|
863
|
+
async quickLogTime(data) {
|
|
864
|
+
const minutes = shorthandToMinutes(data.duration ?? '');
|
|
865
|
+
return this.post('quick/logs/add', {
|
|
866
|
+
account_id: this.accountId,
|
|
867
|
+
user_id: this.userId,
|
|
868
|
+
task_id: data.task_id,
|
|
869
|
+
note: data.note,
|
|
870
|
+
log_time: friendlyDuration(minutes),
|
|
871
|
+
log_date: usDate(data.log_date),
|
|
872
|
+
isnoteonly: minutes <= 0,
|
|
873
|
+
billable: data.billable ?? false,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
// -- Personal logs -------------------------------------------
|
|
877
|
+
/** Personal (non-task) time logs for a user -- the `vs logs list` feed. */
|
|
878
|
+
async getPersonalLogs(userId) {
|
|
879
|
+
return this.get(`personal/logs/user/${userId || this.userId}`);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Add a personal time log: work that belongs to a service rather than to any
|
|
883
|
+
* task. start_date and end_date mirror log_date, which is what the web form
|
|
884
|
+
* posts for a single-day entry.
|
|
885
|
+
*/
|
|
886
|
+
async addPersonalLog(data) {
|
|
887
|
+
const date = usDate(data.log_date);
|
|
888
|
+
return this.post('personal/log/add', {
|
|
889
|
+
account_id: this.accountId,
|
|
890
|
+
user_id: this.userId,
|
|
891
|
+
service_id: data.service_id ?? 0,
|
|
892
|
+
note: data.note,
|
|
893
|
+
log_time: friendlyDuration(shorthandToMinutes(data.duration ?? '')),
|
|
894
|
+
log_date: date,
|
|
895
|
+
start_date: date,
|
|
896
|
+
end_date: date,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
async removePersonalLog(logId) {
|
|
900
|
+
return this.get(`personal/log/remove/${logId}`);
|
|
901
|
+
}
|
|
902
|
+
// -- Time clock ----------------------------------------------
|
|
903
|
+
/**
|
|
904
|
+
* Punch the time clock. type "in" opens a punch, "out" closes the open one;
|
|
905
|
+
* both go to the same route with clocked_in set accordingly.
|
|
906
|
+
*/
|
|
907
|
+
async addPunch(type, note = '') {
|
|
908
|
+
return this.post('user/punch/add', {
|
|
909
|
+
type,
|
|
910
|
+
note,
|
|
911
|
+
user_id: this.userId,
|
|
912
|
+
account_id: this.accountId,
|
|
913
|
+
platform_account_id: this.accountId,
|
|
914
|
+
punch_time: nowIso(),
|
|
915
|
+
clocked_in: type === 'in',
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
async getPunchHistory(userId) {
|
|
919
|
+
return this.get(`user/punch/history?user_id=${userId || this.userId}&account_id=${this.accountId}`);
|
|
920
|
+
}
|
|
784
921
|
// -- Notes --------------------------------------------------
|
|
785
922
|
async getMyNotes() {
|
|
786
923
|
return this.get('notes/me');
|
package/dist/auth/permissions.js
CHANGED
|
@@ -60,6 +60,16 @@ export function featureForPath(path) {
|
|
|
60
60
|
return 'insights';
|
|
61
61
|
if (p.startsWith('logs') || p.startsWith('timelog'))
|
|
62
62
|
return 'logs';
|
|
63
|
+
// Quick task logging and personal logs are the same Pro-tier logging feature.
|
|
64
|
+
if (p.startsWith('quick/logs') || p.startsWith('personal/log'))
|
|
65
|
+
return 'logs';
|
|
66
|
+
if (p.startsWith('ticket'))
|
|
67
|
+
return 'tickets';
|
|
68
|
+
if (p.startsWith('user/punch'))
|
|
69
|
+
return 'time';
|
|
70
|
+
// Task completion route sits outside the tasks/* prefix.
|
|
71
|
+
if (p.startsWith('complete/task'))
|
|
72
|
+
return 'tasks';
|
|
63
73
|
if (p.startsWith('prospect'))
|
|
64
74
|
return 'crm';
|
|
65
75
|
if (p.startsWith('task/comment'))
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ function invalidateClient() {
|
|
|
35
35
|
}
|
|
36
36
|
const server = new McpServer({
|
|
37
37
|
name: 'viviscape',
|
|
38
|
-
version: '2.
|
|
38
|
+
version: '2.9.0',
|
|
39
39
|
});
|
|
40
40
|
function json(data) {
|
|
41
41
|
return JSON.stringify(data, null, 2);
|
|
@@ -894,6 +894,35 @@ server.tool('task_merge', 'Merge one task into another. The source task is consu
|
|
|
894
894
|
const result = await requireClient().mergeTasks(from_task_id, to_task_id);
|
|
895
895
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
896
896
|
});
|
|
897
|
+
server.tool('task_complete', 'Close a task through the platform completion route. Prefer this over task_update with status "completed": it also stamps who finished the work and runs the milestone/progress rollup the task board reads.', {
|
|
898
|
+
task_id: z.number().describe('Task ID to complete'),
|
|
899
|
+
user_id: z.number().optional().describe('User completing the task. Defaults to the signed-in user.'),
|
|
900
|
+
...idempotencyArg,
|
|
901
|
+
}, async ({ task_id, user_id, idempotency_key }) => {
|
|
902
|
+
const result = await once('task_complete', idempotency_key, () => requireClient().completeTask(task_id, user_id));
|
|
903
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
904
|
+
});
|
|
905
|
+
// ============================================================
|
|
906
|
+
// TICKETS
|
|
907
|
+
// ============================================================
|
|
908
|
+
server.tool('tickets_list', 'The account ticket queue, filtered server-side by status set, company and team. status "open" (default) covers backlog, new, research, inprogress, testing, inreview and onhold; "closed" reads the completed/cancelled route; "all" drops the status filter. Tickets are tasks, so rows are task rows -- read one with task_get and search by keyword with task_search. Requires the enterprise plan.', {
|
|
909
|
+
status: z.enum(['open', 'closed', 'all']).optional().describe('Which ticket queue to read (default: open)'),
|
|
910
|
+
statuses: z.array(z.enum(TASK_STATUSES)).optional().describe('Exact status set, overriding the status shorthand'),
|
|
911
|
+
query: z.string().optional().describe('Search text matched server-side'),
|
|
912
|
+
company_id: z.number().optional().describe('Only tickets for this company'),
|
|
913
|
+
team: z.string().optional().describe('Team filter passed through to the API'),
|
|
914
|
+
...pageArgs,
|
|
915
|
+
}, async (params) => {
|
|
916
|
+
const result = await requireClient().lookupTickets({
|
|
917
|
+
status: params.status,
|
|
918
|
+
statuses: params.statuses,
|
|
919
|
+
query: params.query,
|
|
920
|
+
company_id: params.company_id,
|
|
921
|
+
team: params.team,
|
|
922
|
+
});
|
|
923
|
+
const rows = Array.isArray(result) ? annotateTasks(result) : result;
|
|
924
|
+
return { content: [{ type: 'text', text: json(shape(since(rows, params), TASK_FIELDS, paging(params))) }] };
|
|
925
|
+
});
|
|
897
926
|
// ============================================================
|
|
898
927
|
// TASK COMMENTS
|
|
899
928
|
// ============================================================
|
|
@@ -965,6 +994,56 @@ server.tool('timelog_update', 'Update an existing time log entry. Builds full LO
|
|
|
965
994
|
const result = await requireClient().updateTimeLog(params);
|
|
966
995
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
967
996
|
});
|
|
997
|
+
server.tool('task_log_time', 'Log time against a task the short way: the quick route takes the project and service from the task itself, so only the note matters. Omit duration (or pass 0) to post a note-only entry -- it lands on the task time feed without adding hours. Use timelog_add when the entry needs an explicit project, service or user.', {
|
|
998
|
+
task_id: z.number().describe('Task ID to log against'),
|
|
999
|
+
note: z.string().describe('What was worked on'),
|
|
1000
|
+
duration: z.string().optional().describe('Duration in shorthand (e.g., 1h30m, 45m, 2h). Omit for a note-only entry.'),
|
|
1001
|
+
log_date: z.string().optional().describe('Date of the work (ISO 8601). Defaults to today.'),
|
|
1002
|
+
billable: z.boolean().optional().describe('Whether this time is billable (default false)'),
|
|
1003
|
+
...idempotencyArg,
|
|
1004
|
+
}, async (params) => {
|
|
1005
|
+
const result = await once('task_log_time', params.idempotency_key, () => requireClient().quickLogTime(params));
|
|
1006
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
1007
|
+
});
|
|
1008
|
+
// ============================================================
|
|
1009
|
+
// PERSONAL LOGS AND TIME CLOCK
|
|
1010
|
+
// ============================================================
|
|
1011
|
+
server.tool('personal_logs', 'Personal time logs for a user -- work booked against a service rather than any task.', {
|
|
1012
|
+
user_id: z.number().optional().describe('User whose logs to read. Defaults to the signed-in user.'),
|
|
1013
|
+
...pageArgs,
|
|
1014
|
+
}, async (params) => {
|
|
1015
|
+
const result = await requireClient().getPersonalLogs(params.user_id);
|
|
1016
|
+
return { content: [{ type: 'text', text: json(shape(since(result, params), [], paging(params))) }] };
|
|
1017
|
+
});
|
|
1018
|
+
server.tool('personal_log_add', 'Add a personal time log: work that belongs to a service rather than to a task. For task work use task_log_time or timelog_add.', {
|
|
1019
|
+
note: z.string().describe('What was worked on'),
|
|
1020
|
+
duration: z.string().optional().describe('Duration in shorthand (e.g., 1h30m, 45m, 2h)'),
|
|
1021
|
+
service_id: z.number().optional().describe('Service ID classifying the work type'),
|
|
1022
|
+
log_date: z.string().optional().describe('Date of the work (ISO 8601). Defaults to today.'),
|
|
1023
|
+
...idempotencyArg,
|
|
1024
|
+
}, async (params) => {
|
|
1025
|
+
const result = await once('personal_log_add', params.idempotency_key, () => requireClient().addPersonalLog(params));
|
|
1026
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
1027
|
+
});
|
|
1028
|
+
server.tool('personal_log_remove', 'Delete a personal time log by its log_id (from personal_logs).', { log_id: z.number().describe('Personal log ID to remove') }, async ({ log_id }) => {
|
|
1029
|
+
const result = await requireClient().removePersonalLog(log_id);
|
|
1030
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
1031
|
+
});
|
|
1032
|
+
server.tool('timeclock_punch', 'Punch the time clock for the signed-in user. type "in" opens a punch, "out" closes the open one. This is the attendance clock, separate from task time logs.', {
|
|
1033
|
+
type: z.enum(['in', 'out']).describe('Punch direction'),
|
|
1034
|
+
note: z.string().optional().describe('Optional punch note'),
|
|
1035
|
+
...idempotencyArg,
|
|
1036
|
+
}, async ({ type, note, idempotency_key }) => {
|
|
1037
|
+
const result = await once('timeclock_punch', idempotency_key, () => requireClient().addPunch(type, note ?? ''));
|
|
1038
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
1039
|
+
});
|
|
1040
|
+
server.tool('timeclock_history', 'Time clock punch history for a user -- clock-in/clock-out pairs with their notes.', {
|
|
1041
|
+
user_id: z.number().optional().describe('User whose punches to read. Defaults to the signed-in user.'),
|
|
1042
|
+
...pageArgs,
|
|
1043
|
+
}, async (params) => {
|
|
1044
|
+
const result = await requireClient().getPunchHistory(params.user_id);
|
|
1045
|
+
return { content: [{ type: 'text', text: json(shape(since(result, params), [], paging(params))) }] };
|
|
1046
|
+
});
|
|
968
1047
|
// ============================================================
|
|
969
1048
|
// NOTES
|
|
970
1049
|
// ============================================================
|
package/package.json
CHANGED