viviscape-mcp 1.0.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 +71 -0
- package/dist/api-client.d.ts +69 -0
- package/dist/api-client.js +414 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +458 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ViviScape LLC
|
|
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,71 @@
|
|
|
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
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
Set the following environment variables:
|
|
22
|
+
|
|
23
|
+
| Variable | Required | Default | Description |
|
|
24
|
+
|----------|----------|---------|-------------|
|
|
25
|
+
| `VIVISCAPE_API_KEY` | yes | — | Your ViviScape API key |
|
|
26
|
+
| `VIVISCAPE_API_URL` | no | `https://api.viviscape.io` | API base URL |
|
|
27
|
+
|
|
28
|
+
## Claude Desktop / Claude Code
|
|
29
|
+
|
|
30
|
+
Add to your MCP client config (e.g. `claude_desktop_config.json` or `.mcp.json`):
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"viviscape": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "viviscape-mcp"],
|
|
38
|
+
"env": {
|
|
39
|
+
"VIVISCAPE_API_KEY": "your-api-key-here"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Tools
|
|
47
|
+
|
|
48
|
+
The server exposes tools across these domains:
|
|
49
|
+
|
|
50
|
+
- **Prospects** — `prospect_add`, `prospect_get`, `prospect_update`, `prospect_query`, follow-ups, notes
|
|
51
|
+
- **Clients** — `client_add`, `client_get`, `client_get_by_email`, `clients_by_company`
|
|
52
|
+
- **Companies** — `company_add`, `company_list`, `company_update`
|
|
53
|
+
- **Projects** — `project_get`, `project_list`, `project_list_active`, `project_staff`, `project_tasks`, `projects_by_company`
|
|
54
|
+
- **Tasks** — `task_add`, `task_get`, `task_update`, `tasks_open`, `tasks_pending`, `tasks_by_company`, `tasks_by_milestone`
|
|
55
|
+
- **Time logs** — `timelog_add`, `timelog_update`
|
|
56
|
+
- **Notes** — `note_add`, `note_get`, `note_update`, `note_remove`, `notes_mine`, `notes_query`
|
|
57
|
+
- **Insights** — hours by person/service/project, AI summary, person stats, time totals
|
|
58
|
+
- **Account** — `account_info`, `account_services`, `account_users`
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install
|
|
64
|
+
npm run dev # run with tsx, hot reload
|
|
65
|
+
npm run build # compile to dist/
|
|
66
|
+
npm start # run compiled
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export declare class ViviScapeClient {
|
|
2
|
+
private baseUrl;
|
|
3
|
+
private apiKey;
|
|
4
|
+
constructor(baseUrl: string, apiKey: string);
|
|
5
|
+
private request;
|
|
6
|
+
addProspect(data: Record<string, unknown>): Promise<unknown>;
|
|
7
|
+
updateProspect(data: Record<string, unknown>): Promise<unknown>;
|
|
8
|
+
getProspect(prospectId: number): Promise<unknown>;
|
|
9
|
+
getProspectsByRep(repId: number): Promise<unknown>;
|
|
10
|
+
getProspectsByAccount(): Promise<unknown>;
|
|
11
|
+
queryProspects(query?: string, accountId?: number, userId?: number, stages?: string[]): Promise<unknown>;
|
|
12
|
+
removeProspect(prospectId: number): Promise<unknown>;
|
|
13
|
+
addProspectNote(data: Record<string, unknown>): Promise<unknown>;
|
|
14
|
+
updateProspectNote(data: Record<string, unknown>): Promise<unknown>;
|
|
15
|
+
getProspectNotes(prospectId: number): Promise<unknown>;
|
|
16
|
+
removeProspectNote(noteId: number): Promise<unknown>;
|
|
17
|
+
addFollowup(data: Record<string, unknown>): Promise<unknown>;
|
|
18
|
+
updateFollowup(data: Record<string, unknown>): Promise<unknown>;
|
|
19
|
+
getFollowups(prospectId: number): Promise<unknown>;
|
|
20
|
+
listCompanies(): Promise<unknown>;
|
|
21
|
+
addCompany(data: Record<string, unknown>): Promise<unknown>;
|
|
22
|
+
updateCompany(data: Record<string, unknown>): Promise<unknown>;
|
|
23
|
+
getClientsByCompany(companyId: number): Promise<unknown>;
|
|
24
|
+
addClient(data: Record<string, unknown>): Promise<unknown>;
|
|
25
|
+
getClientById(clientId: number): Promise<unknown>;
|
|
26
|
+
getClientByEmail(data: {
|
|
27
|
+
email: string;
|
|
28
|
+
}): Promise<unknown>;
|
|
29
|
+
getMyProjects(): Promise<unknown>;
|
|
30
|
+
getActiveProjects(): Promise<Record<string, unknown>[]>;
|
|
31
|
+
getProjectById(projectId: number): Promise<unknown>;
|
|
32
|
+
getProjectStaff(projectId: number): Promise<unknown>;
|
|
33
|
+
addTask(data: Record<string, unknown>): Promise<unknown>;
|
|
34
|
+
updateTask(data: Record<string, unknown>): Promise<unknown>;
|
|
35
|
+
getProjectTasks(projectId: number): Promise<unknown>;
|
|
36
|
+
getOpenTasks(): Promise<unknown>;
|
|
37
|
+
getTask(taskId: number): Promise<unknown>;
|
|
38
|
+
getPendingTasks(userId: number): Promise<unknown>;
|
|
39
|
+
getTasksByMilestone(milestoneId: number): Promise<unknown>;
|
|
40
|
+
getTasksByGroupAndUser(groupId: number, userId: number): Promise<unknown>;
|
|
41
|
+
getCompanyTasks(companyId: number): Promise<unknown>;
|
|
42
|
+
getProjectsByCompany(userId: number, companyId: number): Promise<unknown>;
|
|
43
|
+
getActiveProjectsByUser(userId: number): Promise<unknown>;
|
|
44
|
+
addTimeLog(data: Record<string, unknown>): Promise<unknown>;
|
|
45
|
+
updateTimeLog(data: Record<string, unknown>): Promise<unknown>;
|
|
46
|
+
getMyNotes(): Promise<unknown>;
|
|
47
|
+
getNoteById(noteId: number): Promise<unknown>;
|
|
48
|
+
addNote(data: Record<string, unknown>): Promise<unknown>;
|
|
49
|
+
updateNote(data: Record<string, unknown>): Promise<unknown>;
|
|
50
|
+
removeNote(noteId: number): Promise<unknown>;
|
|
51
|
+
queryNotes(data: Record<string, unknown>): Promise<unknown>;
|
|
52
|
+
getAccountInfo(): Promise<unknown>;
|
|
53
|
+
getActiveServices(): Promise<unknown>;
|
|
54
|
+
getAllServices(): Promise<unknown>;
|
|
55
|
+
getUsers(): Promise<unknown>;
|
|
56
|
+
getHoursByPerson(data: Record<string, unknown>): Promise<unknown>;
|
|
57
|
+
getTimeLogsByDateRange(data: Record<string, unknown>): Promise<unknown>;
|
|
58
|
+
getPersonStats(data: Record<string, unknown>): Promise<unknown>;
|
|
59
|
+
getHoursByPersonAndService(data: Record<string, unknown>): Promise<unknown>;
|
|
60
|
+
getAllTimeHoursByPersonAndService(data: Record<string, unknown>): Promise<unknown>;
|
|
61
|
+
getProjectHoursByService(data: Record<string, unknown>): Promise<unknown>;
|
|
62
|
+
getAccountHoursByService(data: Record<string, unknown>): Promise<unknown>;
|
|
63
|
+
getAiLogSummary(data: Record<string, unknown>): Promise<unknown>;
|
|
64
|
+
getPersonHoursThisWeek(userId: number): Promise<unknown>;
|
|
65
|
+
getPersonHoursThisMonth(userId: number): Promise<unknown>;
|
|
66
|
+
getPersonHoursLastMonth(userId: number): Promise<unknown>;
|
|
67
|
+
getProjectTimeTotal(projectId: number): Promise<unknown>;
|
|
68
|
+
getAllUsersTime(): Promise<unknown>;
|
|
69
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
const DEFAULT_USER_ID = 210; // Art Hicks
|
|
2
|
+
/**
|
|
3
|
+
* Parse shorthand duration (e.g., "3h", "1h30m", "45m") to HH:MM:SS TimeSpan format.
|
|
4
|
+
*/
|
|
5
|
+
function parseShorthandDuration(input) {
|
|
6
|
+
const raw = input.trim();
|
|
7
|
+
const hMatch = raw.match(/(\d+)\s*h/i);
|
|
8
|
+
const mMatch = raw.match(/(\d+)\s*m/i);
|
|
9
|
+
const hours = hMatch ? parseInt(hMatch[1], 10) : 0;
|
|
10
|
+
const minutes = mMatch ? parseInt(mMatch[1], 10) : 0;
|
|
11
|
+
// If already in HH:MM:SS format, pass through
|
|
12
|
+
if (/^\d{1,2}:\d{2}:\d{2}$/.test(raw)) {
|
|
13
|
+
return { duration: raw, log_time: raw };
|
|
14
|
+
}
|
|
15
|
+
const hh = String(hours).padStart(2, '0');
|
|
16
|
+
const mm = String(minutes).padStart(2, '0');
|
|
17
|
+
return { duration: `${hh}:${mm}:00`, log_time: raw };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build a full Group_Task payload from simplified MCP tool params.
|
|
21
|
+
*/
|
|
22
|
+
function buildTaskPayload(data) {
|
|
23
|
+
const now = new Date().toISOString();
|
|
24
|
+
const assignees = [];
|
|
25
|
+
const userId = data.assigned_to ?? DEFAULT_USER_ID;
|
|
26
|
+
assignees.push({
|
|
27
|
+
assignee_id: 0,
|
|
28
|
+
task_id: 0,
|
|
29
|
+
user_id: userId,
|
|
30
|
+
client_id: 0,
|
|
31
|
+
leader: true,
|
|
32
|
+
attention: false,
|
|
33
|
+
promoting: false,
|
|
34
|
+
pinned: false,
|
|
35
|
+
notify: true,
|
|
36
|
+
active: false,
|
|
37
|
+
activitys: [],
|
|
38
|
+
duration: '00:00:00',
|
|
39
|
+
duration_friendly: '-',
|
|
40
|
+
});
|
|
41
|
+
// Convert estimated_hours shorthand to ticks (1 tick = 100ns, 1h = 36_000_000_000 ticks)
|
|
42
|
+
let estimatedTicks = 0;
|
|
43
|
+
if (data.estimated_hours) {
|
|
44
|
+
const parsed = parseShorthandDuration(String(data.estimated_hours));
|
|
45
|
+
const [h, m] = parsed.duration.split(':').map(Number);
|
|
46
|
+
estimatedTicks = ((h * 60 + m) * 60) * 10_000_000;
|
|
47
|
+
}
|
|
48
|
+
const priorityMap = { low: 1, medium: 2, high: 3, critical: 4 };
|
|
49
|
+
const priorityStr = String(data.priority || 'medium');
|
|
50
|
+
const priorityRank = priorityMap[priorityStr] ?? 2;
|
|
51
|
+
return {
|
|
52
|
+
task_id: 0,
|
|
53
|
+
group_id: data.project_id ?? 0,
|
|
54
|
+
company_id: data.company_id ?? 0,
|
|
55
|
+
service_id: data.service_id ?? 0,
|
|
56
|
+
task: data.title ?? '',
|
|
57
|
+
description: data.description ?? '',
|
|
58
|
+
notes: data.notes ?? '',
|
|
59
|
+
status: data.status ?? 'new',
|
|
60
|
+
priority: priorityStr,
|
|
61
|
+
priority_rank: priorityRank,
|
|
62
|
+
start: data.start_date ?? now,
|
|
63
|
+
end: data.due_date ?? now,
|
|
64
|
+
creation_date: now,
|
|
65
|
+
complete_date: '1901-01-01T00:00:00+00:00',
|
|
66
|
+
estimated_ticks: estimatedTicks,
|
|
67
|
+
task_type: data.task_type ?? 'task',
|
|
68
|
+
isbillable: data.isbillable ?? false,
|
|
69
|
+
creator_id: userId,
|
|
70
|
+
milestone_id: data.milestone_id ?? 0,
|
|
71
|
+
client_id: 0,
|
|
72
|
+
event_id: 0,
|
|
73
|
+
account_id: 0,
|
|
74
|
+
percentage: 0,
|
|
75
|
+
attention: false,
|
|
76
|
+
pinned: false,
|
|
77
|
+
assign_all_staff: false,
|
|
78
|
+
agg_cost: 0,
|
|
79
|
+
agg_rate: 0,
|
|
80
|
+
predecessor_id: 0,
|
|
81
|
+
predecessor_complete: false,
|
|
82
|
+
predecessor_name: '',
|
|
83
|
+
solution: '',
|
|
84
|
+
meta: '',
|
|
85
|
+
agent_id: '00000000-0000-0000-0000-000000000000',
|
|
86
|
+
unit_id: '00000000-0000-0000-0000-000000000000',
|
|
87
|
+
reviewer_id: 0,
|
|
88
|
+
review_required: false,
|
|
89
|
+
parts: '',
|
|
90
|
+
staff_assignees: assignees,
|
|
91
|
+
client_assignees: [],
|
|
92
|
+
task_files: [],
|
|
93
|
+
logs: [],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Build update fields for a Group_Task, mapping friendly names to API names.
|
|
98
|
+
*/
|
|
99
|
+
function buildTaskUpdatePayload(data) {
|
|
100
|
+
const mapped = {};
|
|
101
|
+
if (data.task_id !== undefined)
|
|
102
|
+
mapped.task_id = data.task_id;
|
|
103
|
+
if (data.title !== undefined)
|
|
104
|
+
mapped.task = data.title;
|
|
105
|
+
if (data.description !== undefined)
|
|
106
|
+
mapped.description = data.description;
|
|
107
|
+
if (data.status !== undefined)
|
|
108
|
+
mapped.status = data.status;
|
|
109
|
+
if (data.priority !== undefined) {
|
|
110
|
+
mapped.priority = data.priority;
|
|
111
|
+
const priorityMap = { low: 1, medium: 2, high: 3, critical: 4 };
|
|
112
|
+
mapped.priority_rank = priorityMap[String(data.priority)] ?? 2;
|
|
113
|
+
}
|
|
114
|
+
if (data.due_date !== undefined)
|
|
115
|
+
mapped.end = data.due_date;
|
|
116
|
+
if (data.assigned_to !== undefined) {
|
|
117
|
+
mapped.staff_assignees = [{
|
|
118
|
+
assignee_id: 0,
|
|
119
|
+
task_id: data.task_id ?? 0,
|
|
120
|
+
user_id: data.assigned_to,
|
|
121
|
+
client_id: 0,
|
|
122
|
+
leader: true,
|
|
123
|
+
attention: false,
|
|
124
|
+
promoting: false,
|
|
125
|
+
pinned: false,
|
|
126
|
+
notify: true,
|
|
127
|
+
active: false,
|
|
128
|
+
activitys: [],
|
|
129
|
+
duration: '00:00:00',
|
|
130
|
+
duration_friendly: '-',
|
|
131
|
+
}];
|
|
132
|
+
}
|
|
133
|
+
return mapped;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build a full LOG_Time payload from simplified MCP tool params.
|
|
137
|
+
*/
|
|
138
|
+
function buildTimeLogPayload(data) {
|
|
139
|
+
const now = new Date().toISOString();
|
|
140
|
+
const durInput = String(data.duration || '0h');
|
|
141
|
+
const { duration, log_time } = parseShorthandDuration(durInput);
|
|
142
|
+
return {
|
|
143
|
+
log_id: data.log_id ?? 0,
|
|
144
|
+
account_id: data.account_id ?? 1,
|
|
145
|
+
service_id: data.service_id ?? 0,
|
|
146
|
+
user_id: data.user_id ?? DEFAULT_USER_ID,
|
|
147
|
+
company_id: data.company_id ?? 0,
|
|
148
|
+
client_id: data.client_id ?? 0,
|
|
149
|
+
group_id: data.group_id ?? 0,
|
|
150
|
+
event_id: 0,
|
|
151
|
+
topic_id: 0,
|
|
152
|
+
thread_id: 0,
|
|
153
|
+
task_id: data.task_id ?? 0,
|
|
154
|
+
duration,
|
|
155
|
+
log_time,
|
|
156
|
+
allowance: '00:00:00',
|
|
157
|
+
allowance_ticks: 0,
|
|
158
|
+
note: data.description ?? data.note ?? '',
|
|
159
|
+
log_date: data.log_date ?? now,
|
|
160
|
+
date_added: now,
|
|
161
|
+
expire_date: '0001-01-01T00:00:00',
|
|
162
|
+
isnoteonly: false,
|
|
163
|
+
cost: 0,
|
|
164
|
+
rate: 0,
|
|
165
|
+
net: 0,
|
|
166
|
+
exclude: false,
|
|
167
|
+
billable: data.billable ?? false,
|
|
168
|
+
invoiced: false,
|
|
169
|
+
invoice_id: 0,
|
|
170
|
+
lat: 0,
|
|
171
|
+
lng: 0,
|
|
172
|
+
total_cost: 0,
|
|
173
|
+
total_revenue: 0,
|
|
174
|
+
total_margin: 0,
|
|
175
|
+
agent_id: '00000000-0000-0000-0000-000000000000',
|
|
176
|
+
progress_id: '00000000-0000-0000-0000-000000000000',
|
|
177
|
+
staff: [],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export class ViviScapeClient {
|
|
181
|
+
baseUrl;
|
|
182
|
+
apiKey;
|
|
183
|
+
constructor(baseUrl, apiKey) {
|
|
184
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
185
|
+
this.apiKey = apiKey;
|
|
186
|
+
}
|
|
187
|
+
async request(method, path, body) {
|
|
188
|
+
const url = `${this.baseUrl}${path}`;
|
|
189
|
+
const headers = {
|
|
190
|
+
'ApiKey': this.apiKey,
|
|
191
|
+
'Content-Type': 'application/json',
|
|
192
|
+
};
|
|
193
|
+
const res = await fetch(url, {
|
|
194
|
+
method,
|
|
195
|
+
headers,
|
|
196
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
197
|
+
});
|
|
198
|
+
const text = await res.text();
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
throw new Error(`API ${method} ${path} returned ${res.status}: ${text}`);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(text);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return text;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ── Prospects ──────────────────────────────────────────────
|
|
210
|
+
async addProspect(data) {
|
|
211
|
+
return this.request('POST', '/api/v1/crm/prospect/add', data);
|
|
212
|
+
}
|
|
213
|
+
async updateProspect(data) {
|
|
214
|
+
return this.request('POST', '/api/v1/crm/prospect/update', data);
|
|
215
|
+
}
|
|
216
|
+
async getProspect(prospectId) {
|
|
217
|
+
return this.request('GET', `/api/v1/crm/prospect/id/${prospectId}`);
|
|
218
|
+
}
|
|
219
|
+
async getProspectsByRep(repId) {
|
|
220
|
+
return this.request('GET', `/api/v1/crm/prospects/rep/${repId}`);
|
|
221
|
+
}
|
|
222
|
+
async getProspectsByAccount() {
|
|
223
|
+
return this.request('GET', '/api/v1/crm/prospects/account');
|
|
224
|
+
}
|
|
225
|
+
async queryProspects(query, accountId, userId, stages) {
|
|
226
|
+
return this.request('POST', '/api/v1/crm/prospects/query', {
|
|
227
|
+
query, account_id: accountId, user_id: userId, stages,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async removeProspect(prospectId) {
|
|
231
|
+
return this.request('GET', `/api/v1/crm/prospect/remove/${prospectId}`);
|
|
232
|
+
}
|
|
233
|
+
// ── Prospect Notes ─────────────────────────────────────────
|
|
234
|
+
async addProspectNote(data) {
|
|
235
|
+
return this.request('POST', '/api/v1/crm/prospect/note/add', data);
|
|
236
|
+
}
|
|
237
|
+
async updateProspectNote(data) {
|
|
238
|
+
return this.request('POST', '/api/v1/crm/prospect/note/update', data);
|
|
239
|
+
}
|
|
240
|
+
async getProspectNotes(prospectId) {
|
|
241
|
+
return this.request('GET', `/api/v1/crm/prospect/notes/${prospectId}`);
|
|
242
|
+
}
|
|
243
|
+
async removeProspectNote(noteId) {
|
|
244
|
+
return this.request('GET', `/api/v1/crm/prospect/note/remove/${noteId}`);
|
|
245
|
+
}
|
|
246
|
+
// ── Prospect Followups ─────────────────────────────────────
|
|
247
|
+
async addFollowup(data) {
|
|
248
|
+
return this.request('POST', '/api/v1/crm/prospect/followup/add', data);
|
|
249
|
+
}
|
|
250
|
+
async updateFollowup(data) {
|
|
251
|
+
return this.request('POST', '/api/v1/crm/prospect/followup/update', data);
|
|
252
|
+
}
|
|
253
|
+
async getFollowups(prospectId) {
|
|
254
|
+
return this.request('GET', `/api/v1/crm/prospect/followups/list/${prospectId}`);
|
|
255
|
+
}
|
|
256
|
+
// ── Companies ──────────────────────────────────────────────
|
|
257
|
+
async listCompanies() {
|
|
258
|
+
return this.request('GET', '/api/v1/companies/list');
|
|
259
|
+
}
|
|
260
|
+
async addCompany(data) {
|
|
261
|
+
return this.request('POST', '/api/v1/companies/add', data);
|
|
262
|
+
}
|
|
263
|
+
async updateCompany(data) {
|
|
264
|
+
return this.request('POST', '/api/v1/companies/update', data);
|
|
265
|
+
}
|
|
266
|
+
async getClientsByCompany(companyId) {
|
|
267
|
+
return this.request('GET', `/api/v1/companies/clients/company/${companyId}`);
|
|
268
|
+
}
|
|
269
|
+
async addClient(data) {
|
|
270
|
+
return this.request('POST', '/api/v1/companies/client/add', data);
|
|
271
|
+
}
|
|
272
|
+
async getClientById(clientId) {
|
|
273
|
+
return this.request('GET', `/api/v1/companies/client/id/${clientId}`);
|
|
274
|
+
}
|
|
275
|
+
async getClientByEmail(data) {
|
|
276
|
+
return this.request('POST', '/api/v1/companies/client/email', data);
|
|
277
|
+
}
|
|
278
|
+
// ── Projects ───────────────────────────────────────────────
|
|
279
|
+
async getMyProjects() {
|
|
280
|
+
return this.request('GET', '/api/v1/projects/user/projects');
|
|
281
|
+
}
|
|
282
|
+
async getActiveProjects() {
|
|
283
|
+
const projects = await this.request('GET', '/api/v1/projects/user/projects');
|
|
284
|
+
return projects.filter(p => p.bitActive === true);
|
|
285
|
+
}
|
|
286
|
+
async getProjectById(projectId) {
|
|
287
|
+
return this.request('GET', `/api/v1/projects/id?project_id=${projectId}`);
|
|
288
|
+
}
|
|
289
|
+
async getProjectStaff(projectId) {
|
|
290
|
+
return this.request('GET', `/api/v1/projects/project/staff?project_id=${projectId}`);
|
|
291
|
+
}
|
|
292
|
+
async addTask(data) {
|
|
293
|
+
const payload = buildTaskPayload(data);
|
|
294
|
+
return this.request('POST', '/api/v1/projects/task/add', payload);
|
|
295
|
+
}
|
|
296
|
+
async updateTask(data) {
|
|
297
|
+
if (data.task_id) {
|
|
298
|
+
const existing = await this.getTask(data.task_id);
|
|
299
|
+
const payload = { ...existing, ...buildTaskUpdatePayload(data) };
|
|
300
|
+
return this.request('POST', '/api/v1/projects/task/update', payload);
|
|
301
|
+
}
|
|
302
|
+
return this.request('POST', '/api/v1/projects/task/update', buildTaskUpdatePayload(data));
|
|
303
|
+
}
|
|
304
|
+
async getProjectTasks(projectId) {
|
|
305
|
+
return this.request('GET', `/api/v1/projects/tasks/project?project_id=${projectId}`);
|
|
306
|
+
}
|
|
307
|
+
async getOpenTasks() {
|
|
308
|
+
return this.request('POST', '/api/v1/projects/account/tasks/open', {});
|
|
309
|
+
}
|
|
310
|
+
async getTask(taskId) {
|
|
311
|
+
return this.request('GET', `/api/v1/projects/task/get?task_id=${taskId}`);
|
|
312
|
+
}
|
|
313
|
+
async getPendingTasks(userId) {
|
|
314
|
+
return this.request('GET', `/api/v1/projects/user/tasks/pending?user_id=${userId}`);
|
|
315
|
+
}
|
|
316
|
+
async getTasksByMilestone(milestoneId) {
|
|
317
|
+
return this.request('GET', `/api/v1/projects/tasks/milestones?milestone_id=${milestoneId}`);
|
|
318
|
+
}
|
|
319
|
+
async getTasksByGroupAndUser(groupId, userId) {
|
|
320
|
+
return this.request('GET', `/api/v1/projects/tasks/group/user?group_id=${groupId}&user_id=${userId}`);
|
|
321
|
+
}
|
|
322
|
+
async getCompanyTasks(companyId) {
|
|
323
|
+
return this.request('GET', `/api/v1/projects/company/tasks?company_id=${companyId}`);
|
|
324
|
+
}
|
|
325
|
+
async getProjectsByCompany(userId, companyId) {
|
|
326
|
+
return this.request('GET', `/api/v1/projects/user/projects/company?user_id=${userId}&companyid=${companyId}`);
|
|
327
|
+
}
|
|
328
|
+
async getActiveProjectsByUser(userId) {
|
|
329
|
+
return this.request('GET', `/api/v1/projects/user/projects/active?user_id=${userId}`);
|
|
330
|
+
}
|
|
331
|
+
async addTimeLog(data) {
|
|
332
|
+
const payload = buildTimeLogPayload(data);
|
|
333
|
+
return this.request('POST', '/api/v1/projects/task/logs/add', payload);
|
|
334
|
+
}
|
|
335
|
+
async updateTimeLog(data) {
|
|
336
|
+
if (data.log_id) {
|
|
337
|
+
const payload = buildTimeLogPayload(data);
|
|
338
|
+
return this.request('POST', '/api/v1/projects/task/logs/update', payload);
|
|
339
|
+
}
|
|
340
|
+
return this.request('POST', '/api/v1/projects/task/logs/update', buildTimeLogPayload(data));
|
|
341
|
+
}
|
|
342
|
+
// ── Notes ──────────────────────────────────────────────────
|
|
343
|
+
async getMyNotes() {
|
|
344
|
+
return this.request('GET', '/api/v1/notes/me');
|
|
345
|
+
}
|
|
346
|
+
async getNoteById(noteId) {
|
|
347
|
+
return this.request('GET', `/api/v1/notes/id/${noteId}`);
|
|
348
|
+
}
|
|
349
|
+
async addNote(data) {
|
|
350
|
+
return this.request('POST', '/api/v1/notes/add', data);
|
|
351
|
+
}
|
|
352
|
+
async updateNote(data) {
|
|
353
|
+
return this.request('POST', '/api/v1/notes/update', data);
|
|
354
|
+
}
|
|
355
|
+
async removeNote(noteId) {
|
|
356
|
+
return this.request('GET', `/api/v1/notes/remove/${noteId}`);
|
|
357
|
+
}
|
|
358
|
+
async queryNotes(data) {
|
|
359
|
+
return this.request('POST', '/api/v1/notes/query', data);
|
|
360
|
+
}
|
|
361
|
+
// ── Account ────────────────────────────────────────────────
|
|
362
|
+
async getAccountInfo() {
|
|
363
|
+
return this.request('GET', '/api/v1/account/info');
|
|
364
|
+
}
|
|
365
|
+
async getActiveServices() {
|
|
366
|
+
return this.request('GET', '/api/v1/account/services/active');
|
|
367
|
+
}
|
|
368
|
+
async getAllServices() {
|
|
369
|
+
return this.request('GET', '/api/v1/account/services/list');
|
|
370
|
+
}
|
|
371
|
+
async getUsers() {
|
|
372
|
+
return this.request('GET', '/api/v1/account/users');
|
|
373
|
+
}
|
|
374
|
+
// ── Insights ───────────────────────────────────────────────
|
|
375
|
+
async getHoursByPerson(data) {
|
|
376
|
+
return this.request('POST', '/api/v1/insights/logs/hours/person', data);
|
|
377
|
+
}
|
|
378
|
+
async getTimeLogsByDateRange(data) {
|
|
379
|
+
return this.request('POST', '/api/v1/insights/logs/account/daterange', data);
|
|
380
|
+
}
|
|
381
|
+
async getPersonStats(data) {
|
|
382
|
+
return this.request('POST', '/api/v1/insights/person/stats', data);
|
|
383
|
+
}
|
|
384
|
+
async getHoursByPersonAndService(data) {
|
|
385
|
+
return this.request('POST', '/api/v1/insights/logs/hours/person/service', data);
|
|
386
|
+
}
|
|
387
|
+
async getAllTimeHoursByPersonAndService(data) {
|
|
388
|
+
return this.request('POST', '/api/v1/insights/logs/hours/alltime/person/service', data);
|
|
389
|
+
}
|
|
390
|
+
async getProjectHoursByService(data) {
|
|
391
|
+
return this.request('POST', '/api/v1/insights/logs/project/hours/service', data);
|
|
392
|
+
}
|
|
393
|
+
async getAccountHoursByService(data) {
|
|
394
|
+
return this.request('POST', '/api/v1/insights/logs/account/hours/service', data);
|
|
395
|
+
}
|
|
396
|
+
async getAiLogSummary(data) {
|
|
397
|
+
return this.request('POST', '/api/v1/insights/ai/log/summary', data);
|
|
398
|
+
}
|
|
399
|
+
async getPersonHoursThisWeek(userId) {
|
|
400
|
+
return this.request('GET', `/api/v1/insights/insights/eval/person/week/${userId}`);
|
|
401
|
+
}
|
|
402
|
+
async getPersonHoursThisMonth(userId) {
|
|
403
|
+
return this.request('GET', `/api/v1/insights/eval/person/month/${userId}`);
|
|
404
|
+
}
|
|
405
|
+
async getPersonHoursLastMonth(userId) {
|
|
406
|
+
return this.request('GET', `/api/v1/insights/eval/person/lastmonth/${userId}`);
|
|
407
|
+
}
|
|
408
|
+
async getProjectTimeTotal(projectId) {
|
|
409
|
+
return this.request('GET', `/api/v1/insights/eval/project/all/${projectId}`);
|
|
410
|
+
}
|
|
411
|
+
async getAllUsersTime() {
|
|
412
|
+
return this.request('GET', '/api/v1/insights/eval/users/all');
|
|
413
|
+
}
|
|
414
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { config } from 'dotenv';
|
|
6
|
+
import { ViviScapeClient } from './api-client.js';
|
|
7
|
+
config();
|
|
8
|
+
const apiUrl = process.env.VIVISCAPE_API_URL || 'https://api.viviscape.io';
|
|
9
|
+
const apiKey = process.env.VIVISCAPE_API_KEY || '';
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
console.error('VIVISCAPE_API_KEY is required. Set it in .env or environment.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const client = new ViviScapeClient(apiUrl, apiKey);
|
|
15
|
+
const server = new McpServer({
|
|
16
|
+
name: 'viviscape',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
});
|
|
19
|
+
function json(data) {
|
|
20
|
+
return JSON.stringify(data, null, 2);
|
|
21
|
+
}
|
|
22
|
+
// ════════════════════════════════════════════════════════════
|
|
23
|
+
// PROSPECTS
|
|
24
|
+
// ════════════════════════════════════════════════════════════
|
|
25
|
+
server.tool('prospect_add', 'Add a new prospect/lead to the ViviScape CRM', {
|
|
26
|
+
first_name: z.string().describe('First name'),
|
|
27
|
+
last_name: z.string().describe('Last name'),
|
|
28
|
+
email: z.string().describe('Email address'),
|
|
29
|
+
phone: z.string().optional().describe('Phone number'),
|
|
30
|
+
company: z.string().optional().describe('Company name'),
|
|
31
|
+
org: z.string().optional().describe('Organization'),
|
|
32
|
+
note: z.string().optional().describe('Notes about the prospect'),
|
|
33
|
+
source: z.string().optional().describe('Lead source (e.g., "Website", "ROI Calculator")'),
|
|
34
|
+
source_url: z.string().optional().describe('URL where the lead came from'),
|
|
35
|
+
referred_by: z.string().optional().describe('Referral source'),
|
|
36
|
+
status: z.string().optional().describe('Status: new, firstcontact, negotiation, pending, won, lost, spam'),
|
|
37
|
+
}, async (params) => {
|
|
38
|
+
const result = await client.addProspect(params);
|
|
39
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
40
|
+
});
|
|
41
|
+
server.tool('prospect_update', 'Update an existing prospect in the CRM', {
|
|
42
|
+
prospect_id: z.number().describe('Prospect ID to update'),
|
|
43
|
+
first_name: z.string().optional(),
|
|
44
|
+
last_name: z.string().optional(),
|
|
45
|
+
email: z.string().optional(),
|
|
46
|
+
phone: z.string().optional(),
|
|
47
|
+
company: z.string().optional(),
|
|
48
|
+
org: z.string().optional(),
|
|
49
|
+
note: z.string().optional(),
|
|
50
|
+
source: z.string().optional(),
|
|
51
|
+
status: z.string().optional().describe('Status: new, firstcontact, negotiation, pending, won, lost, spam'),
|
|
52
|
+
priority: z.number().optional().describe('Priority level'),
|
|
53
|
+
}, async (params) => {
|
|
54
|
+
const result = await client.updateProspect(params);
|
|
55
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
56
|
+
});
|
|
57
|
+
server.tool('prospect_get', 'Get a prospect by ID', { prospect_id: z.number().describe('Prospect ID') }, async ({ prospect_id }) => {
|
|
58
|
+
const result = await client.getProspect(prospect_id);
|
|
59
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
60
|
+
});
|
|
61
|
+
server.tool('prospect_query', 'Search/query prospects with filters', {
|
|
62
|
+
query: z.string().optional().describe('Search text (name, email, company)'),
|
|
63
|
+
account_id: z.number().optional().describe('Filter by account ID'),
|
|
64
|
+
user_id: z.number().optional().describe('Filter by user/rep ID'),
|
|
65
|
+
stages: z.array(z.string()).optional().describe('Filter by stages: new, firstcontact, negotiation, pending, won, lost, spam'),
|
|
66
|
+
}, async (params) => {
|
|
67
|
+
const result = await client.queryProspects(params.query, params.account_id, params.user_id, params.stages);
|
|
68
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
69
|
+
});
|
|
70
|
+
server.tool('prospect_list_by_account', 'Get all prospects for the current account', {}, async () => {
|
|
71
|
+
const result = await client.getProspectsByAccount();
|
|
72
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
73
|
+
});
|
|
74
|
+
server.tool('prospect_list_by_rep', 'Get all prospects assigned to a specific sales rep', { rep_id: z.number().describe('Sales rep user ID') }, async ({ rep_id }) => {
|
|
75
|
+
const result = await client.getProspectsByRep(rep_id);
|
|
76
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
77
|
+
});
|
|
78
|
+
server.tool('prospect_remove', 'Remove a prospect from the CRM', { prospect_id: z.number().describe('Prospect ID to remove') }, async ({ prospect_id }) => {
|
|
79
|
+
const result = await client.removeProspect(prospect_id);
|
|
80
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
81
|
+
});
|
|
82
|
+
// ════════════════════════════════════════════════════════════
|
|
83
|
+
// PROSPECT NOTES
|
|
84
|
+
// ════════════════════════════════════════════════════════════
|
|
85
|
+
server.tool('prospect_note_add', 'Add a note to a prospect', {
|
|
86
|
+
prospect_id: z.number().describe('Prospect ID'),
|
|
87
|
+
note: z.string().describe('Note content'),
|
|
88
|
+
author: z.string().optional().describe('Author name'),
|
|
89
|
+
}, async (params) => {
|
|
90
|
+
const result = await client.addProspectNote(params);
|
|
91
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
92
|
+
});
|
|
93
|
+
server.tool('prospect_note_update', 'Update a prospect note', {
|
|
94
|
+
note_id: z.number().describe('Note ID to update'),
|
|
95
|
+
note: z.string().describe('Updated note content'),
|
|
96
|
+
}, async (params) => {
|
|
97
|
+
const result = await client.updateProspectNote(params);
|
|
98
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
99
|
+
});
|
|
100
|
+
server.tool('prospect_notes_list', 'Get all notes for a prospect', { prospect_id: z.number().describe('Prospect ID') }, async ({ prospect_id }) => {
|
|
101
|
+
const result = await client.getProspectNotes(prospect_id);
|
|
102
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
103
|
+
});
|
|
104
|
+
server.tool('prospect_note_remove', 'Remove a prospect note', { note_id: z.number().describe('Note ID to remove') }, async ({ note_id }) => {
|
|
105
|
+
const result = await client.removeProspectNote(note_id);
|
|
106
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
107
|
+
});
|
|
108
|
+
// ════════════════════════════════════════════════════════════
|
|
109
|
+
// PROSPECT FOLLOWUPS
|
|
110
|
+
// ════════════════════════════════════════════════════════════
|
|
111
|
+
server.tool('prospect_followup_add', 'Add a followup record for a prospect', {
|
|
112
|
+
prospect_id: z.number().describe('Prospect ID'),
|
|
113
|
+
contact_method: z.string().optional().describe('Contact method (e.g., email, phone, meeting)'),
|
|
114
|
+
outcome: z.string().optional().describe('Outcome of the followup'),
|
|
115
|
+
subject: z.string().optional().describe('Subject/title'),
|
|
116
|
+
content: z.string().optional().describe('Followup content/details'),
|
|
117
|
+
followup_date: z.string().optional().describe('Followup date (ISO 8601)'),
|
|
118
|
+
}, async (params) => {
|
|
119
|
+
const result = await client.addFollowup(params);
|
|
120
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
121
|
+
});
|
|
122
|
+
server.tool('prospect_followup_update', 'Update a followup record', {
|
|
123
|
+
followup_id: z.number().describe('Followup ID to update'),
|
|
124
|
+
contact_method: z.string().optional(),
|
|
125
|
+
outcome: z.string().optional(),
|
|
126
|
+
subject: z.string().optional(),
|
|
127
|
+
content: z.string().optional(),
|
|
128
|
+
followup_date: z.string().optional(),
|
|
129
|
+
}, async (params) => {
|
|
130
|
+
const result = await client.updateFollowup(params);
|
|
131
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
132
|
+
});
|
|
133
|
+
server.tool('prospect_followups_list', 'Get all followup records for a prospect', { prospect_id: z.number().describe('Prospect ID') }, async ({ prospect_id }) => {
|
|
134
|
+
const result = await client.getFollowups(prospect_id);
|
|
135
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
136
|
+
});
|
|
137
|
+
// ════════════════════════════════════════════════════════════
|
|
138
|
+
// COMPANIES & CLIENTS
|
|
139
|
+
// ════════════════════════════════════════════════════════════
|
|
140
|
+
server.tool('company_list', 'List all companies', {}, async () => {
|
|
141
|
+
const result = await client.listCompanies();
|
|
142
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
143
|
+
});
|
|
144
|
+
server.tool('company_add', 'Add a new company', {
|
|
145
|
+
name: z.string().describe('Company name'),
|
|
146
|
+
address1: z.string().optional(),
|
|
147
|
+
address2: z.string().optional(),
|
|
148
|
+
city: z.string().optional(),
|
|
149
|
+
state: z.string().optional(),
|
|
150
|
+
zip: z.string().optional(),
|
|
151
|
+
phone: z.string().optional(),
|
|
152
|
+
website: z.string().optional(),
|
|
153
|
+
}, async (params) => {
|
|
154
|
+
const result = await client.addCompany(params);
|
|
155
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
156
|
+
});
|
|
157
|
+
server.tool('company_update', 'Update an existing company', {
|
|
158
|
+
company_id: z.number().describe('Company ID to update'),
|
|
159
|
+
name: z.string().optional(),
|
|
160
|
+
address1: z.string().optional(),
|
|
161
|
+
city: z.string().optional(),
|
|
162
|
+
state: z.string().optional(),
|
|
163
|
+
zip: z.string().optional(),
|
|
164
|
+
phone: z.string().optional(),
|
|
165
|
+
website: z.string().optional(),
|
|
166
|
+
}, async (params) => {
|
|
167
|
+
const result = await client.updateCompany(params);
|
|
168
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
169
|
+
});
|
|
170
|
+
server.tool('client_get', 'Get a client by ID', { client_id: z.number().describe('Client ID') }, async ({ client_id }) => {
|
|
171
|
+
const result = await client.getClientById(client_id);
|
|
172
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
173
|
+
});
|
|
174
|
+
server.tool('client_get_by_email', 'Look up a client by email address', { email: z.string().describe('Email address') }, async ({ email }) => {
|
|
175
|
+
const result = await client.getClientByEmail({ email });
|
|
176
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
177
|
+
});
|
|
178
|
+
server.tool('client_add', 'Add a new client to a company', {
|
|
179
|
+
company_id: z.number().describe('Company ID'),
|
|
180
|
+
first_name: z.string().describe('First name'),
|
|
181
|
+
last_name: z.string().describe('Last name'),
|
|
182
|
+
email: z.string().describe('Email'),
|
|
183
|
+
phone: z.string().optional(),
|
|
184
|
+
title: z.string().optional().describe('Job title'),
|
|
185
|
+
}, async (params) => {
|
|
186
|
+
const result = await client.addClient(params);
|
|
187
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
188
|
+
});
|
|
189
|
+
server.tool('clients_by_company', 'Get all clients for a company', { company_id: z.number().describe('Company ID') }, async ({ company_id }) => {
|
|
190
|
+
const result = await client.getClientsByCompany(company_id);
|
|
191
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
192
|
+
});
|
|
193
|
+
// ════════════════════════════════════════════════════════════
|
|
194
|
+
// PROJECTS & TASKS
|
|
195
|
+
// ════════════════════════════════════════════════════════════
|
|
196
|
+
server.tool('project_list', 'Get all projects for current user', {}, async () => {
|
|
197
|
+
const result = await client.getMyProjects();
|
|
198
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
199
|
+
});
|
|
200
|
+
server.tool('project_list_active', 'Get active projects for current user', {}, async () => {
|
|
201
|
+
const result = await client.getActiveProjects();
|
|
202
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
203
|
+
});
|
|
204
|
+
server.tool('project_get', 'Get a project by ID', { project_id: z.number().describe('Project ID') }, async ({ project_id }) => {
|
|
205
|
+
const result = await client.getProjectById(project_id);
|
|
206
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
207
|
+
});
|
|
208
|
+
server.tool('project_staff', 'Get staff members assigned to a project', { project_id: z.number().describe('Project ID') }, async ({ project_id }) => {
|
|
209
|
+
const result = await client.getProjectStaff(project_id);
|
|
210
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
211
|
+
});
|
|
212
|
+
server.tool('project_tasks', 'Get all tasks for a project', { project_id: z.number().describe('Project ID') }, async ({ project_id }) => {
|
|
213
|
+
const result = await client.getProjectTasks(project_id);
|
|
214
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
215
|
+
});
|
|
216
|
+
server.tool('tasks_open', 'Get all open tasks across projects', {}, async () => {
|
|
217
|
+
const result = await client.getOpenTasks();
|
|
218
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
219
|
+
});
|
|
220
|
+
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.', {
|
|
221
|
+
project_id: z.number().describe('Project ID (group_id)'),
|
|
222
|
+
title: z.string().describe('Task title'),
|
|
223
|
+
description: z.string().optional().describe('Task description'),
|
|
224
|
+
notes: z.string().optional().describe('Task notes'),
|
|
225
|
+
assigned_to: z.number().optional().describe('User ID to assign to (also sets creator). Defaults to Art Hicks (210)'),
|
|
226
|
+
company_id: z.number().optional().describe('Company ID associated with the project'),
|
|
227
|
+
service_id: z.number().optional().describe('Service ID classifying the work type'),
|
|
228
|
+
priority: z.string().optional().describe('Priority: low, medium, high, critical'),
|
|
229
|
+
estimated_hours: z.string().optional().describe('Estimated time in shorthand (e.g., 6h, 2h30m)'),
|
|
230
|
+
start_date: z.string().optional().describe('Start date (ISO 8601)'),
|
|
231
|
+
due_date: z.string().optional().describe('Due date (ISO 8601)'),
|
|
232
|
+
milestone_id: z.number().optional().describe('Milestone ID'),
|
|
233
|
+
}, async (params) => {
|
|
234
|
+
const result = await client.addTask(params);
|
|
235
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
236
|
+
});
|
|
237
|
+
server.tool('task_update', 'Update a task. Fetches existing task first and merges changes.', {
|
|
238
|
+
task_id: z.number().describe('Task ID to update'),
|
|
239
|
+
title: z.string().optional().describe('Task title'),
|
|
240
|
+
description: z.string().optional(),
|
|
241
|
+
assigned_to: z.number().optional().describe('User ID to assign to'),
|
|
242
|
+
priority: z.string().optional().describe('Priority: low, medium, high, critical'),
|
|
243
|
+
status: z.string().optional().describe('Status: new, inprogress, complete, onhold'),
|
|
244
|
+
due_date: z.string().optional().describe('Due date (ISO 8601)'),
|
|
245
|
+
}, async (params) => {
|
|
246
|
+
const result = await client.updateTask(params);
|
|
247
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
248
|
+
});
|
|
249
|
+
server.tool('task_get', 'Get a single task by ID', { task_id: z.number().describe('Task ID') }, async ({ task_id }) => {
|
|
250
|
+
const result = await client.getTask(task_id);
|
|
251
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
252
|
+
});
|
|
253
|
+
server.tool('tasks_pending', 'Get pending tasks for a user', { user_id: z.number().describe('User ID') }, async ({ user_id }) => {
|
|
254
|
+
const result = await client.getPendingTasks(user_id);
|
|
255
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
256
|
+
});
|
|
257
|
+
server.tool('tasks_by_milestone', 'Get tasks for a milestone', { milestone_id: z.number().describe('Milestone ID') }, async ({ milestone_id }) => {
|
|
258
|
+
const result = await client.getTasksByMilestone(milestone_id);
|
|
259
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
260
|
+
});
|
|
261
|
+
server.tool('tasks_by_group_user', 'Get tasks by project group and user', {
|
|
262
|
+
group_id: z.number().describe('Project group ID'),
|
|
263
|
+
user_id: z.number().describe('User ID'),
|
|
264
|
+
}, async ({ group_id, user_id }) => {
|
|
265
|
+
const result = await client.getTasksByGroupAndUser(group_id, user_id);
|
|
266
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
267
|
+
});
|
|
268
|
+
server.tool('tasks_by_company', 'Get tasks for a company', { company_id: z.number().describe('Company ID') }, async ({ company_id }) => {
|
|
269
|
+
const result = await client.getCompanyTasks(company_id);
|
|
270
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
271
|
+
});
|
|
272
|
+
server.tool('projects_by_company', 'Get user projects filtered by company', {
|
|
273
|
+
user_id: z.number().describe('User ID'),
|
|
274
|
+
company_id: z.number().describe('Company ID'),
|
|
275
|
+
}, async ({ user_id, company_id }) => {
|
|
276
|
+
const result = await client.getProjectsByCompany(user_id, company_id);
|
|
277
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
278
|
+
});
|
|
279
|
+
server.tool('projects_active_by_user', 'Get active projects for a specific user', { user_id: z.number().describe('User ID') }, async ({ user_id }) => {
|
|
280
|
+
const result = await client.getActiveProjectsByUser(user_id);
|
|
281
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
282
|
+
});
|
|
283
|
+
// ════════════════════════════════════════════════════════════
|
|
284
|
+
// TIME TRACKING
|
|
285
|
+
// ════════════════════════════════════════════════════════════
|
|
286
|
+
server.tool('timelog_add', 'Log time against a task. Duration uses shorthand format (e.g., 1h30m, 30m, 2h). Builds full LOG_Time payload.', {
|
|
287
|
+
task_id: z.number().describe('Task ID to log time against'),
|
|
288
|
+
user_id: z.number().optional().describe('User ID who performed the work. Defaults to Art Hicks (210)'),
|
|
289
|
+
duration: z.string().describe('Duration in shorthand format (e.g., 1h30m, 30m, 2h)'),
|
|
290
|
+
service_id: z.number().optional().describe('Service ID classifying the work type'),
|
|
291
|
+
group_id: z.number().optional().describe('Project/group ID'),
|
|
292
|
+
company_id: z.number().optional().describe('Company ID'),
|
|
293
|
+
log_date: z.string().optional().describe('Date of the work (ISO 8601)'),
|
|
294
|
+
description: z.string().optional().describe('Description of work performed'),
|
|
295
|
+
billable: z.boolean().optional().describe('Whether this time is billable'),
|
|
296
|
+
}, async (params) => {
|
|
297
|
+
const result = await client.addTimeLog(params);
|
|
298
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
299
|
+
});
|
|
300
|
+
server.tool('timelog_update', 'Update an existing time log entry. Builds full LOG_Time payload.', {
|
|
301
|
+
log_id: z.number().describe('Time log ID to update'),
|
|
302
|
+
task_id: z.number().optional().describe('Task ID'),
|
|
303
|
+
user_id: z.number().optional().describe('User ID'),
|
|
304
|
+
duration: z.string().optional().describe('Duration in shorthand format (e.g., 1h30m, 30m, 2h)'),
|
|
305
|
+
service_id: z.number().optional().describe('Service ID classifying the work type'),
|
|
306
|
+
group_id: z.number().optional().describe('Project/group ID'),
|
|
307
|
+
company_id: z.number().optional().describe('Company ID'),
|
|
308
|
+
log_date: z.string().optional().describe('Date of the work (ISO 8601)'),
|
|
309
|
+
description: z.string().optional().describe('Description of work performed'),
|
|
310
|
+
billable: z.boolean().optional().describe('Whether this time is billable'),
|
|
311
|
+
}, async (params) => {
|
|
312
|
+
const result = await client.updateTimeLog(params);
|
|
313
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
314
|
+
});
|
|
315
|
+
// ════════════════════════════════════════════════════════════
|
|
316
|
+
// NOTES
|
|
317
|
+
// ════════════════════════════════════════════════════════════
|
|
318
|
+
server.tool('notes_mine', 'Get all notes for the current user', {}, async () => {
|
|
319
|
+
const result = await client.getMyNotes();
|
|
320
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
321
|
+
});
|
|
322
|
+
server.tool('note_get', 'Get a note by ID', { note_id: z.number().describe('Note ID') }, async ({ note_id }) => {
|
|
323
|
+
const result = await client.getNoteById(note_id);
|
|
324
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
325
|
+
});
|
|
326
|
+
server.tool('note_add', 'Create a new note', {
|
|
327
|
+
note: z.string().describe('Note content'),
|
|
328
|
+
title: z.string().optional().describe('Note title'),
|
|
329
|
+
}, async (params) => {
|
|
330
|
+
const result = await client.addNote(params);
|
|
331
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
332
|
+
});
|
|
333
|
+
server.tool('note_update', 'Update a note', {
|
|
334
|
+
note_id: z.number().describe('Note ID to update'),
|
|
335
|
+
note: z.string().describe('Updated content'),
|
|
336
|
+
title: z.string().optional(),
|
|
337
|
+
}, async (params) => {
|
|
338
|
+
const result = await client.updateNote(params);
|
|
339
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
340
|
+
});
|
|
341
|
+
server.tool('note_remove', 'Delete a note', { note_id: z.number().describe('Note ID to remove') }, async ({ note_id }) => {
|
|
342
|
+
const result = await client.removeNote(note_id);
|
|
343
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
344
|
+
});
|
|
345
|
+
server.tool('notes_query', 'Search notes', {
|
|
346
|
+
query: z.string().optional().describe('Search text'),
|
|
347
|
+
}, async (params) => {
|
|
348
|
+
const result = await client.queryNotes(params);
|
|
349
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
350
|
+
});
|
|
351
|
+
// ════════════════════════════════════════════════════════════
|
|
352
|
+
// ACCOUNT
|
|
353
|
+
// ════════════════════════════════════════════════════════════
|
|
354
|
+
server.tool('account_info', 'Get account information', {}, async () => {
|
|
355
|
+
const result = await client.getAccountInfo();
|
|
356
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
357
|
+
});
|
|
358
|
+
server.tool('account_users', 'Get all users in the account', {}, async () => {
|
|
359
|
+
const result = await client.getUsers();
|
|
360
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
361
|
+
});
|
|
362
|
+
server.tool('account_services', 'Get active services for the account', {}, async () => {
|
|
363
|
+
const result = await client.getActiveServices();
|
|
364
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
365
|
+
});
|
|
366
|
+
// ════════════════════════════════════════════════════════════
|
|
367
|
+
// INSIGHTS
|
|
368
|
+
// ════════════════════════════════════════════════════════════
|
|
369
|
+
server.tool('insights_hours_by_person', 'Get logged hours by person', {
|
|
370
|
+
user_id: z.number().describe('User ID'),
|
|
371
|
+
start_date: z.string().describe('Start date (ISO 8601)'),
|
|
372
|
+
end_date: z.string().describe('End date (ISO 8601)'),
|
|
373
|
+
}, async (params) => {
|
|
374
|
+
const result = await client.getHoursByPerson(params);
|
|
375
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
376
|
+
});
|
|
377
|
+
server.tool('insights_timelogs', 'Get time logs for the account within a date range', {
|
|
378
|
+
start_date: z.string().describe('Start date (ISO 8601)'),
|
|
379
|
+
end_date: z.string().describe('End date (ISO 8601)'),
|
|
380
|
+
}, async (params) => {
|
|
381
|
+
const result = await client.getTimeLogsByDateRange(params);
|
|
382
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
383
|
+
});
|
|
384
|
+
server.tool('insights_person_stats', 'Get stats for a person', {
|
|
385
|
+
user_id: z.number().describe('User ID'),
|
|
386
|
+
start_date: z.string().optional().describe('Start date (ISO 8601)'),
|
|
387
|
+
end_date: z.string().optional().describe('End date (ISO 8601)'),
|
|
388
|
+
}, async (params) => {
|
|
389
|
+
const result = await client.getPersonStats(params);
|
|
390
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
391
|
+
});
|
|
392
|
+
server.tool('insights_hours_by_person_service', 'Get logged hours by person and service type', {
|
|
393
|
+
user_id: z.number().describe('User ID'),
|
|
394
|
+
start_date: z.string().describe('Start date (ISO 8601)'),
|
|
395
|
+
end_date: z.string().describe('End date (ISO 8601)'),
|
|
396
|
+
service_id: z.number().optional().describe('Service ID to filter by'),
|
|
397
|
+
}, async (params) => {
|
|
398
|
+
const result = await client.getHoursByPersonAndService(params);
|
|
399
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
400
|
+
});
|
|
401
|
+
server.tool('insights_alltime_hours_person_service', 'Get all-time hours per person and service', {
|
|
402
|
+
user_id: z.number().describe('User ID'),
|
|
403
|
+
service_id: z.number().optional().describe('Service ID to filter by'),
|
|
404
|
+
}, async (params) => {
|
|
405
|
+
const result = await client.getAllTimeHoursByPersonAndService(params);
|
|
406
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
407
|
+
});
|
|
408
|
+
server.tool('insights_project_hours_by_service', 'Get hours by project broken down by service type', {
|
|
409
|
+
project_id: z.number().describe('Project ID'),
|
|
410
|
+
start_date: z.string().optional().describe('Start date (ISO 8601)'),
|
|
411
|
+
end_date: z.string().optional().describe('End date (ISO 8601)'),
|
|
412
|
+
}, async (params) => {
|
|
413
|
+
const result = await client.getProjectHoursByService(params);
|
|
414
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
415
|
+
});
|
|
416
|
+
server.tool('insights_account_hours_by_service', 'Get account hours broken down by service type', {
|
|
417
|
+
start_date: z.string().describe('Start date (ISO 8601)'),
|
|
418
|
+
end_date: z.string().describe('End date (ISO 8601)'),
|
|
419
|
+
}, async (params) => {
|
|
420
|
+
const result = await client.getAccountHoursByService(params);
|
|
421
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
422
|
+
});
|
|
423
|
+
server.tool('insights_ai_summary', 'Get an AI-generated summary of time logs', {
|
|
424
|
+
user_id: z.number().optional().describe('User ID'),
|
|
425
|
+
start_date: z.string().optional().describe('Start date (ISO 8601)'),
|
|
426
|
+
end_date: z.string().optional().describe('End date (ISO 8601)'),
|
|
427
|
+
}, async (params) => {
|
|
428
|
+
const result = await client.getAiLogSummary(params);
|
|
429
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
430
|
+
});
|
|
431
|
+
server.tool('insights_person_hours_week', 'Get a person\'s logged hours for the current week', { user_id: z.number().describe('User ID') }, async ({ user_id }) => {
|
|
432
|
+
const result = await client.getPersonHoursThisWeek(user_id);
|
|
433
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
434
|
+
});
|
|
435
|
+
server.tool('insights_person_hours_month', 'Get a person\'s logged hours for the current month', { user_id: z.number().describe('User ID') }, async ({ user_id }) => {
|
|
436
|
+
const result = await client.getPersonHoursThisMonth(user_id);
|
|
437
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
438
|
+
});
|
|
439
|
+
server.tool('insights_person_hours_lastmonth', 'Get a person\'s logged hours for last month', { user_id: z.number().describe('User ID') }, async ({ user_id }) => {
|
|
440
|
+
const result = await client.getPersonHoursLastMonth(user_id);
|
|
441
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
442
|
+
});
|
|
443
|
+
server.tool('insights_project_time_total', 'Get total time logged for a project', { project_id: z.number().describe('Project ID') }, async ({ project_id }) => {
|
|
444
|
+
const result = await client.getProjectTimeTotal(project_id);
|
|
445
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
446
|
+
});
|
|
447
|
+
server.tool('insights_all_users_time', 'Get time totals for all users', {}, async () => {
|
|
448
|
+
const result = await client.getAllUsersTime();
|
|
449
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
450
|
+
});
|
|
451
|
+
// ════════════════════════════════════════════════════════════
|
|
452
|
+
// START
|
|
453
|
+
// ════════════════════════════════════════════════════════════
|
|
454
|
+
async function main() {
|
|
455
|
+
const transport = new StdioServerTransport();
|
|
456
|
+
await server.connect(transport);
|
|
457
|
+
}
|
|
458
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "viviscape-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the ViviScape API — CRM, projects, companies, notes, and insights",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"viviscape-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"viviscape",
|
|
25
|
+
"crm",
|
|
26
|
+
"claude"
|
|
27
|
+
],
|
|
28
|
+
"author": "ViviScape LLC",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
35
|
+
"dotenv": "^17.3.1",
|
|
36
|
+
"zod": "^4.3.6"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.5.0",
|
|
40
|
+
"tsx": "^4.21.0",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
}
|
|
43
|
+
}
|