@opsrev/acculynx-cli 1.9.2 → 1.11.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/dist/chunk-FEKZ3BED.js +302 -0
- package/dist/index.js +92 -248
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.js +288 -0
- package/package.json +5 -3
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var BASE_URL = "https://api.acculynx.com/api/v2";
|
|
3
|
+
function getConfig(options) {
|
|
4
|
+
const apiKey = options.apiKey ?? process.env.ACCULYNX_API_KEY;
|
|
5
|
+
if (!apiKey) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"Missing required credential: ACCULYNX_API_KEY (set env var or pass --api-key)"
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
baseUrl: BASE_URL,
|
|
12
|
+
apiKey
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/api-client.ts
|
|
17
|
+
var MAX_RETRIES = 3;
|
|
18
|
+
var INITIAL_DELAY_MS = 1e3;
|
|
19
|
+
async function sleep(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
async function fetchWithRetry(url, init, retries = MAX_RETRIES) {
|
|
23
|
+
const response = await fetch(url, init);
|
|
24
|
+
if (response.status === 429 && retries > 0) {
|
|
25
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
26
|
+
const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_DELAY_MS * Math.pow(2, MAX_RETRIES - retries);
|
|
27
|
+
await sleep(delay);
|
|
28
|
+
return fetchWithRetry(url, init, retries - 1);
|
|
29
|
+
}
|
|
30
|
+
return response;
|
|
31
|
+
}
|
|
32
|
+
function createApiClient(config) {
|
|
33
|
+
const headers = {
|
|
34
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
35
|
+
Accept: "application/json"
|
|
36
|
+
};
|
|
37
|
+
async function doGet(path, params) {
|
|
38
|
+
let url = `${config.baseUrl}${path}`;
|
|
39
|
+
if (params && Object.keys(params).length > 0) {
|
|
40
|
+
url += `?${new URLSearchParams(params).toString()}`;
|
|
41
|
+
}
|
|
42
|
+
const response = await fetchWithRetry(url, { headers });
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const text = await response.text();
|
|
45
|
+
throw new Error(
|
|
46
|
+
`API error (${response.status} ${response.statusText}): ${text}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return response.json();
|
|
50
|
+
}
|
|
51
|
+
async function doPost(path, body) {
|
|
52
|
+
const url = `${config.baseUrl}${path}`;
|
|
53
|
+
const response = await fetchWithRetry(url, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify(body)
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const text2 = await response.text();
|
|
60
|
+
throw new Error(
|
|
61
|
+
`API error (${response.status} ${response.statusText}): ${text2}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const text = await response.text();
|
|
65
|
+
return text ? JSON.parse(text) : { status: response.status };
|
|
66
|
+
}
|
|
67
|
+
async function doPostForm(path, body) {
|
|
68
|
+
const url = `${config.baseUrl}${path}`;
|
|
69
|
+
const { "Content-Type": _, ...formHeaders } = headers;
|
|
70
|
+
const response = await fetchWithRetry(url, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: formHeaders,
|
|
73
|
+
body
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const text2 = await response.text();
|
|
77
|
+
throw new Error(
|
|
78
|
+
`API error (${response.status} ${response.statusText}): ${text2}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const text = await response.text();
|
|
82
|
+
return text ? JSON.parse(text) : { status: response.status };
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
get: doGet,
|
|
86
|
+
post: doPost,
|
|
87
|
+
postForm: doPostForm
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/ops/ping.ts
|
|
92
|
+
function ping(client) {
|
|
93
|
+
return client.get("/diagnostics/ping");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/api-helpers.ts
|
|
97
|
+
var DEFAULT_PAGE_SIZE = 25;
|
|
98
|
+
var DEFAULT_LIMIT = 25;
|
|
99
|
+
async function paginate(client, path, params, limit = DEFAULT_LIMIT) {
|
|
100
|
+
const results = [];
|
|
101
|
+
let pageStartIndex = 0;
|
|
102
|
+
while (true) {
|
|
103
|
+
const pageParams = {
|
|
104
|
+
...params,
|
|
105
|
+
pageSize: String(DEFAULT_PAGE_SIZE),
|
|
106
|
+
pageStartIndex: String(pageStartIndex)
|
|
107
|
+
};
|
|
108
|
+
const data = await client.get(path, pageParams);
|
|
109
|
+
results.push(...data.items);
|
|
110
|
+
if (limit !== Infinity && results.length >= limit) {
|
|
111
|
+
return results.slice(0, limit);
|
|
112
|
+
}
|
|
113
|
+
if (data.items.length < DEFAULT_PAGE_SIZE) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
pageStartIndex += DEFAULT_PAGE_SIZE;
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
async function readStdin(stream = process.stdin) {
|
|
121
|
+
const chunks = [];
|
|
122
|
+
for await (const chunk of stream) {
|
|
123
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
124
|
+
}
|
|
125
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
126
|
+
if (!raw) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
"No input provided on stdin. Pipe JSON body, e.g.: echo '{...}' | acculynx jobs create"
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(raw);
|
|
133
|
+
} catch {
|
|
134
|
+
throw new Error("Invalid JSON on stdin");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/ops/jobs.ts
|
|
139
|
+
import { readFileSync } from "fs";
|
|
140
|
+
import { basename, extname } from "path";
|
|
141
|
+
var DISALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
142
|
+
".exe",
|
|
143
|
+
".com",
|
|
144
|
+
".dll",
|
|
145
|
+
".msi",
|
|
146
|
+
".bat",
|
|
147
|
+
".cmd",
|
|
148
|
+
".sh",
|
|
149
|
+
".pl",
|
|
150
|
+
".vbs",
|
|
151
|
+
".py",
|
|
152
|
+
".php"
|
|
153
|
+
]);
|
|
154
|
+
function jobsList(client, opts = {}) {
|
|
155
|
+
const params = {};
|
|
156
|
+
if (opts.startDate) params.startDate = opts.startDate;
|
|
157
|
+
if (opts.endDate) params.endDate = opts.endDate;
|
|
158
|
+
if (opts.dateFilterType) params.dateFilterType = opts.dateFilterType;
|
|
159
|
+
if (opts.milestones) params.milestones = opts.milestones;
|
|
160
|
+
params.sortBy = opts.sortBy ?? "CreatedDate";
|
|
161
|
+
if (opts.sortOrder) params.sortOrder = opts.sortOrder;
|
|
162
|
+
if (opts.includes) params.includes = opts.includes;
|
|
163
|
+
if (opts.assignment) params.assignment = opts.assignment;
|
|
164
|
+
return paginate(client, "/jobs", params, opts.limit);
|
|
165
|
+
}
|
|
166
|
+
function jobGet(client, jobId) {
|
|
167
|
+
return client.get(`/jobs/${jobId}`);
|
|
168
|
+
}
|
|
169
|
+
function jobCreate(client, body) {
|
|
170
|
+
return client.post("/jobs", body);
|
|
171
|
+
}
|
|
172
|
+
function jobSearch(client, query) {
|
|
173
|
+
return client.post("/jobs/search", { SearchTerm: query });
|
|
174
|
+
}
|
|
175
|
+
var jobContacts = (client, jobId) => client.get(`/jobs/${jobId}/contacts`);
|
|
176
|
+
var jobEstimates = (client, jobId) => client.get(`/jobs/${jobId}/estimates`);
|
|
177
|
+
var jobFinancials = (client, jobId) => client.get(`/jobs/${jobId}/financials`);
|
|
178
|
+
var jobInvoices = (client, jobId) => client.get(`/jobs/${jobId}/invoices`);
|
|
179
|
+
var jobMilestones = (client, jobId) => client.get(`/jobs/${jobId}/milestone-history`);
|
|
180
|
+
var jobPayments = (client, jobId) => client.get(`/jobs/${jobId}/payments`);
|
|
181
|
+
var jobHistory = (client, jobId) => client.get(`/jobs/${jobId}/history`);
|
|
182
|
+
var jobReps = (client, jobId) => client.get(`/jobs/${jobId}/representatives`);
|
|
183
|
+
function jobRepsAssign(client, jobId, opts) {
|
|
184
|
+
const type = opts.type ?? "company";
|
|
185
|
+
return client.post(`/jobs/${jobId}/representatives/${type}`, { id: opts.userId });
|
|
186
|
+
}
|
|
187
|
+
function documentFolders(client, opts = {}) {
|
|
188
|
+
const params = {
|
|
189
|
+
recordStartIndex: opts.recordStartIndex ?? "0",
|
|
190
|
+
sortOrder: opts.sortOrder ?? "Ascending"
|
|
191
|
+
};
|
|
192
|
+
if (opts.pageSize) params.pageSize = opts.pageSize;
|
|
193
|
+
return client.get("/company-settings/job-file-settings/document-folders", params);
|
|
194
|
+
}
|
|
195
|
+
function jobAddExpense(client, jobId, body) {
|
|
196
|
+
return client.post(`/jobs/${jobId}/payments/expense`, body);
|
|
197
|
+
}
|
|
198
|
+
function jobUploadDocument(client, jobId, filePath, opts) {
|
|
199
|
+
const ext = extname(filePath).toLowerCase();
|
|
200
|
+
if (DISALLOWED_EXTENSIONS.has(ext)) {
|
|
201
|
+
throw new Error(`File type ${ext} is not allowed`);
|
|
202
|
+
}
|
|
203
|
+
const fileBuffer = readFileSync(filePath);
|
|
204
|
+
const fileName = basename(filePath);
|
|
205
|
+
const file = new File([fileBuffer], fileName);
|
|
206
|
+
const form = new FormData();
|
|
207
|
+
form.append("file", file);
|
|
208
|
+
form.append("documentFolderId", opts.folderId);
|
|
209
|
+
if (opts.description) form.append("description", opts.description);
|
|
210
|
+
if (opts.externalId) form.append("externalId", opts.externalId);
|
|
211
|
+
if (opts.externalSource) form.append("externalSource", opts.externalSource);
|
|
212
|
+
return client.postForm(`/jobs/${jobId}/documents`, form);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/ops/contacts.ts
|
|
216
|
+
function contactsList(client, limit) {
|
|
217
|
+
return paginate(client, "/contacts", {}, limit);
|
|
218
|
+
}
|
|
219
|
+
function contactGet(client, contactId) {
|
|
220
|
+
return client.get(`/contacts/${contactId}`);
|
|
221
|
+
}
|
|
222
|
+
function contactCreate(client, body) {
|
|
223
|
+
return client.post("/contacts", body);
|
|
224
|
+
}
|
|
225
|
+
function contactsSearch(client, opts) {
|
|
226
|
+
return client.post("/contacts/search", {
|
|
227
|
+
searchTerm: opts.query,
|
|
228
|
+
startDate: opts.startDate,
|
|
229
|
+
endDate: opts.endDate,
|
|
230
|
+
sort: {
|
|
231
|
+
sortColumn: opts.sortBy ?? "lastName",
|
|
232
|
+
sortDirection: opts.sortOrder ?? "Ascending"
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
var contactEmails = (client, contactId) => client.get(`/contacts/${contactId}/email-addresses`);
|
|
237
|
+
var contactPhones = (client, contactId) => client.get(`/contacts/${contactId}/phone-numbers`);
|
|
238
|
+
function contactPhoneAdd(client, contactId, opts) {
|
|
239
|
+
return client.post(`/contacts/${contactId}/phone-numbers`, {
|
|
240
|
+
type: opts.type,
|
|
241
|
+
number: opts.number,
|
|
242
|
+
smsOptOut: Boolean(opts.smsOptOut)
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/ops/estimates.ts
|
|
247
|
+
function estimatesList(client, limit) {
|
|
248
|
+
return paginate(client, "/estimates", {}, limit);
|
|
249
|
+
}
|
|
250
|
+
var estimateGet = (client, estimateId) => client.get(`/estimates/${estimateId}`);
|
|
251
|
+
var estimateSections = (client, estimateId) => client.get(`/estimates/${estimateId}/sections`);
|
|
252
|
+
var estimateSection = (client, estimateId, sectionId) => client.get(`/estimates/${estimateId}/sections/${sectionId}`);
|
|
253
|
+
var estimateItems = (client, estimateId, sectionId) => client.get(`/estimates/${estimateId}/sections/${sectionId}/items`);
|
|
254
|
+
|
|
255
|
+
// src/ops/users.ts
|
|
256
|
+
async function usersList(client, opts = {}) {
|
|
257
|
+
if (opts.search) {
|
|
258
|
+
const all = await paginate(client, "/users", {}, Infinity);
|
|
259
|
+
const term = opts.search.toLowerCase();
|
|
260
|
+
return all.filter((u) => {
|
|
261
|
+
const fields = [u.displayName, u.firstName, u.lastName, u.email];
|
|
262
|
+
return fields.some((f) => f && f.toLowerCase().includes(term));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return paginate(client, "/users", {}, opts.limit);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export {
|
|
269
|
+
getConfig,
|
|
270
|
+
createApiClient,
|
|
271
|
+
ping,
|
|
272
|
+
readStdin,
|
|
273
|
+
jobsList,
|
|
274
|
+
jobGet,
|
|
275
|
+
jobCreate,
|
|
276
|
+
jobSearch,
|
|
277
|
+
jobContacts,
|
|
278
|
+
jobEstimates,
|
|
279
|
+
jobFinancials,
|
|
280
|
+
jobInvoices,
|
|
281
|
+
jobMilestones,
|
|
282
|
+
jobPayments,
|
|
283
|
+
jobHistory,
|
|
284
|
+
jobReps,
|
|
285
|
+
jobRepsAssign,
|
|
286
|
+
documentFolders,
|
|
287
|
+
jobAddExpense,
|
|
288
|
+
jobUploadDocument,
|
|
289
|
+
contactsList,
|
|
290
|
+
contactGet,
|
|
291
|
+
contactCreate,
|
|
292
|
+
contactsSearch,
|
|
293
|
+
contactEmails,
|
|
294
|
+
contactPhones,
|
|
295
|
+
contactPhoneAdd,
|
|
296
|
+
estimatesList,
|
|
297
|
+
estimateGet,
|
|
298
|
+
estimateSections,
|
|
299
|
+
estimateSection,
|
|
300
|
+
estimateItems,
|
|
301
|
+
usersList
|
|
302
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,262 +1,120 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
contactCreate,
|
|
4
|
+
contactEmails,
|
|
5
|
+
contactGet,
|
|
6
|
+
contactPhoneAdd,
|
|
7
|
+
contactPhones,
|
|
8
|
+
contactsList,
|
|
9
|
+
contactsSearch,
|
|
10
|
+
createApiClient,
|
|
11
|
+
documentFolders,
|
|
12
|
+
estimateGet,
|
|
13
|
+
estimateItems,
|
|
14
|
+
estimateSection,
|
|
15
|
+
estimateSections,
|
|
16
|
+
estimatesList,
|
|
17
|
+
getConfig,
|
|
18
|
+
jobAddExpense,
|
|
19
|
+
jobContacts,
|
|
20
|
+
jobCreate,
|
|
21
|
+
jobEstimates,
|
|
22
|
+
jobFinancials,
|
|
23
|
+
jobGet,
|
|
24
|
+
jobHistory,
|
|
25
|
+
jobInvoices,
|
|
26
|
+
jobMilestones,
|
|
27
|
+
jobPayments,
|
|
28
|
+
jobReps,
|
|
29
|
+
jobRepsAssign,
|
|
30
|
+
jobSearch,
|
|
31
|
+
jobUploadDocument,
|
|
32
|
+
jobsList,
|
|
33
|
+
ping,
|
|
34
|
+
readStdin,
|
|
35
|
+
usersList
|
|
36
|
+
} from "./chunk-FEKZ3BED.js";
|
|
2
37
|
|
|
3
38
|
// src/index.ts
|
|
4
39
|
import { createRequire } from "module";
|
|
5
40
|
import { Command } from "commander";
|
|
6
41
|
|
|
7
|
-
// src/config.ts
|
|
8
|
-
var BASE_URL = "https://api.acculynx.com/api/v2";
|
|
9
|
-
function getConfig(options) {
|
|
10
|
-
const apiKey = options.apiKey ?? process.env.ACCULYNX_API_KEY;
|
|
11
|
-
if (!apiKey) {
|
|
12
|
-
throw new Error(
|
|
13
|
-
"Missing required credential: ACCULYNX_API_KEY (set env var or pass --api-key)"
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
return {
|
|
17
|
-
baseUrl: BASE_URL,
|
|
18
|
-
apiKey
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// src/api-client.ts
|
|
23
|
-
var MAX_RETRIES = 3;
|
|
24
|
-
var INITIAL_DELAY_MS = 1e3;
|
|
25
|
-
async function sleep(ms) {
|
|
26
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
-
}
|
|
28
|
-
async function fetchWithRetry(url, init, retries = MAX_RETRIES) {
|
|
29
|
-
const response = await fetch(url, init);
|
|
30
|
-
if (response.status === 429 && retries > 0) {
|
|
31
|
-
const retryAfter = response.headers.get("Retry-After");
|
|
32
|
-
const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_DELAY_MS * Math.pow(2, MAX_RETRIES - retries);
|
|
33
|
-
await sleep(delay);
|
|
34
|
-
return fetchWithRetry(url, init, retries - 1);
|
|
35
|
-
}
|
|
36
|
-
return response;
|
|
37
|
-
}
|
|
38
|
-
function createApiClient(config) {
|
|
39
|
-
const headers = {
|
|
40
|
-
Authorization: `Bearer ${config.apiKey}`,
|
|
41
|
-
Accept: "application/json"
|
|
42
|
-
};
|
|
43
|
-
async function doGet(path, params) {
|
|
44
|
-
let url = `${config.baseUrl}${path}`;
|
|
45
|
-
if (params && Object.keys(params).length > 0) {
|
|
46
|
-
url += `?${new URLSearchParams(params).toString()}`;
|
|
47
|
-
}
|
|
48
|
-
const response = await fetchWithRetry(url, { headers });
|
|
49
|
-
if (!response.ok) {
|
|
50
|
-
const text = await response.text();
|
|
51
|
-
throw new Error(
|
|
52
|
-
`API error (${response.status} ${response.statusText}): ${text}`
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
return response.json();
|
|
56
|
-
}
|
|
57
|
-
async function doPost(path, body) {
|
|
58
|
-
const url = `${config.baseUrl}${path}`;
|
|
59
|
-
const response = await fetchWithRetry(url, {
|
|
60
|
-
method: "POST",
|
|
61
|
-
headers: { ...headers, "Content-Type": "application/json" },
|
|
62
|
-
body: JSON.stringify(body)
|
|
63
|
-
});
|
|
64
|
-
if (!response.ok) {
|
|
65
|
-
const text2 = await response.text();
|
|
66
|
-
throw new Error(
|
|
67
|
-
`API error (${response.status} ${response.statusText}): ${text2}`
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
const text = await response.text();
|
|
71
|
-
return text ? JSON.parse(text) : { status: response.status };
|
|
72
|
-
}
|
|
73
|
-
async function doPostForm(path, body) {
|
|
74
|
-
const url = `${config.baseUrl}${path}`;
|
|
75
|
-
const { "Content-Type": _, ...formHeaders } = headers;
|
|
76
|
-
const response = await fetchWithRetry(url, {
|
|
77
|
-
method: "POST",
|
|
78
|
-
headers: formHeaders,
|
|
79
|
-
body
|
|
80
|
-
});
|
|
81
|
-
if (!response.ok) {
|
|
82
|
-
const text2 = await response.text();
|
|
83
|
-
throw new Error(
|
|
84
|
-
`API error (${response.status} ${response.statusText}): ${text2}`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
const text = await response.text();
|
|
88
|
-
return text ? JSON.parse(text) : { status: response.status };
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
get: doGet,
|
|
92
|
-
post: doPost,
|
|
93
|
-
postForm: doPostForm
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
42
|
// src/commands/ping.ts
|
|
98
43
|
function registerPingCommand(parentCmd, getClient2) {
|
|
99
44
|
parentCmd.command("ping").description("Health check \u2014 verify API key and connectivity").action(async () => {
|
|
100
|
-
const result = await getClient2()
|
|
45
|
+
const result = await ping(getClient2());
|
|
101
46
|
console.log(JSON.stringify(result));
|
|
102
47
|
});
|
|
103
48
|
}
|
|
104
49
|
|
|
105
50
|
// src/commands/jobs.ts
|
|
106
|
-
import { readFileSync } from "fs";
|
|
107
|
-
import { basename, extname } from "path";
|
|
108
|
-
|
|
109
|
-
// src/api-helpers.ts
|
|
110
|
-
var DEFAULT_PAGE_SIZE = 25;
|
|
111
|
-
var DEFAULT_LIMIT = 25;
|
|
112
|
-
async function paginate(client, path, params, limit = DEFAULT_LIMIT) {
|
|
113
|
-
const results = [];
|
|
114
|
-
let pageStartIndex = 0;
|
|
115
|
-
while (true) {
|
|
116
|
-
const pageParams = {
|
|
117
|
-
...params,
|
|
118
|
-
pageSize: String(DEFAULT_PAGE_SIZE),
|
|
119
|
-
pageStartIndex: String(pageStartIndex)
|
|
120
|
-
};
|
|
121
|
-
const data = await client.get(path, pageParams);
|
|
122
|
-
results.push(...data.items);
|
|
123
|
-
if (limit !== Infinity && results.length >= limit) {
|
|
124
|
-
return results.slice(0, limit);
|
|
125
|
-
}
|
|
126
|
-
if (data.items.length < DEFAULT_PAGE_SIZE) {
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
pageStartIndex += DEFAULT_PAGE_SIZE;
|
|
130
|
-
}
|
|
131
|
-
return results;
|
|
132
|
-
}
|
|
133
|
-
async function readStdin(stream = process.stdin) {
|
|
134
|
-
const chunks = [];
|
|
135
|
-
for await (const chunk of stream) {
|
|
136
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
137
|
-
}
|
|
138
|
-
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
139
|
-
if (!raw) {
|
|
140
|
-
throw new Error(
|
|
141
|
-
"No input provided on stdin. Pipe JSON body, e.g.: echo '{...}' | acculynx jobs create"
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
try {
|
|
145
|
-
return JSON.parse(raw);
|
|
146
|
-
} catch {
|
|
147
|
-
throw new Error("Invalid JSON on stdin");
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// src/commands/jobs.ts
|
|
152
|
-
var DISALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
153
|
-
".exe",
|
|
154
|
-
".com",
|
|
155
|
-
".dll",
|
|
156
|
-
".msi",
|
|
157
|
-
".bat",
|
|
158
|
-
".cmd",
|
|
159
|
-
".sh",
|
|
160
|
-
".pl",
|
|
161
|
-
".vbs",
|
|
162
|
-
".py",
|
|
163
|
-
".php"
|
|
164
|
-
]);
|
|
165
51
|
function registerJobsCommands(parentCmd, getClient2) {
|
|
166
52
|
const jobs = parentCmd.command("jobs").description("Job operations");
|
|
167
53
|
jobs.command("list").description("List jobs (paginated)").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--date-filter-type <type>", "Date field to filter on").option("--milestones <milestones>", "Filter by milestones").option("--sort-by <field>", "Sort by: CreatedDate, MilestoneDate, ModifiedDate").option("--sort-order <order>", "Ascending or Descending").option("--includes <fields>", "Include: contact, initialAppointment").option("--assignment <type>", "Filter by assignment: assigned, unassigned").option("--limit <n>", "Max total results (default: 25)").option("--all", "Fetch all results (no limit)").action(async (opts) => {
|
|
168
|
-
const params = {};
|
|
169
|
-
if (opts.startDate) params.startDate = opts.startDate;
|
|
170
|
-
if (opts.endDate) params.endDate = opts.endDate;
|
|
171
|
-
if (opts.dateFilterType) params.dateFilterType = opts.dateFilterType;
|
|
172
|
-
if (opts.milestones) params.milestones = opts.milestones;
|
|
173
|
-
params.sortBy = opts.sortBy ?? "CreatedDate";
|
|
174
|
-
if (opts.sortOrder) params.sortOrder = opts.sortOrder;
|
|
175
|
-
if (opts.includes) params.includes = opts.includes;
|
|
176
|
-
if (opts.assignment) params.assignment = opts.assignment;
|
|
177
54
|
const limit = opts.all ? Infinity : opts.limit ? parseInt(opts.limit, 10) : void 0;
|
|
178
|
-
const result = await
|
|
55
|
+
const result = await jobsList(getClient2(), {
|
|
56
|
+
startDate: opts.startDate,
|
|
57
|
+
endDate: opts.endDate,
|
|
58
|
+
dateFilterType: opts.dateFilterType,
|
|
59
|
+
milestones: opts.milestones,
|
|
60
|
+
sortBy: opts.sortBy,
|
|
61
|
+
sortOrder: opts.sortOrder,
|
|
62
|
+
includes: opts.includes,
|
|
63
|
+
assignment: opts.assignment,
|
|
64
|
+
limit
|
|
65
|
+
});
|
|
179
66
|
console.log(JSON.stringify(result));
|
|
180
67
|
});
|
|
181
68
|
jobs.command("get").argument("<jobId>", "Job ID").description("Get job details").action(async (jobId) => {
|
|
182
|
-
|
|
183
|
-
console.log(JSON.stringify(result));
|
|
69
|
+
console.log(JSON.stringify(await jobGet(getClient2(), jobId)));
|
|
184
70
|
});
|
|
185
71
|
jobs.command("create").description("Create a job (pipe JSON body to stdin)").action(async () => {
|
|
186
72
|
const body = await readStdin();
|
|
187
|
-
|
|
188
|
-
console.log(JSON.stringify(result));
|
|
73
|
+
console.log(JSON.stringify(await jobCreate(getClient2(), body)));
|
|
189
74
|
});
|
|
190
75
|
jobs.command("search").description("Search jobs").requiredOption("--query <text>", "Search term (required)").action(async (opts) => {
|
|
191
|
-
|
|
192
|
-
SearchTerm: opts.query
|
|
193
|
-
});
|
|
194
|
-
console.log(JSON.stringify(result));
|
|
76
|
+
console.log(JSON.stringify(await jobSearch(getClient2(), opts.query)));
|
|
195
77
|
});
|
|
196
78
|
const subResources = [
|
|
197
|
-
{ name: "contacts",
|
|
198
|
-
{ name: "estimates",
|
|
199
|
-
{ name: "financials",
|
|
200
|
-
{ name: "invoices",
|
|
201
|
-
{ name: "milestones",
|
|
202
|
-
{ name: "payments",
|
|
203
|
-
{ name: "history",
|
|
79
|
+
{ name: "contacts", fn: jobContacts, desc: "List job contacts" },
|
|
80
|
+
{ name: "estimates", fn: jobEstimates, desc: "List job estimates" },
|
|
81
|
+
{ name: "financials", fn: jobFinancials, desc: "Get job financials" },
|
|
82
|
+
{ name: "invoices", fn: jobInvoices, desc: "List job invoices" },
|
|
83
|
+
{ name: "milestones", fn: jobMilestones, desc: "List job milestone history" },
|
|
84
|
+
{ name: "payments", fn: jobPayments, desc: "List job payments" },
|
|
85
|
+
{ name: "history", fn: jobHistory, desc: "Get job history" }
|
|
204
86
|
];
|
|
205
87
|
for (const sub of subResources) {
|
|
206
88
|
jobs.command(sub.name).argument("<jobId>", "Job ID").description(sub.desc).action(async (jobId) => {
|
|
207
|
-
|
|
208
|
-
console.log(JSON.stringify(result));
|
|
89
|
+
console.log(JSON.stringify(await sub.fn(getClient2(), jobId)));
|
|
209
90
|
});
|
|
210
91
|
}
|
|
211
92
|
jobs.command("reps").argument("<jobId>", "Job ID").description("List all representatives for a job").action(async (jobId) => {
|
|
212
|
-
|
|
213
|
-
console.log(JSON.stringify(result));
|
|
93
|
+
console.log(JSON.stringify(await jobReps(getClient2(), jobId)));
|
|
214
94
|
});
|
|
215
95
|
jobs.command("reps-assign").argument("<jobId>", "Job ID").description("Assign a representative to a job").requiredOption("--user-id <id>", "User ID to assign").option("--type <type>", "Rep type: company, sales-owner, ar-owner", "company").action(async (jobId, opts) => {
|
|
216
|
-
const result = await getClient2().
|
|
217
|
-
`/jobs/${jobId}/representatives/${opts.type}`,
|
|
218
|
-
{ id: opts.userId }
|
|
219
|
-
);
|
|
96
|
+
const result = await jobRepsAssign(getClient2(), jobId, { userId: opts.userId, type: opts.type });
|
|
220
97
|
console.log(JSON.stringify(result));
|
|
221
98
|
});
|
|
222
99
|
jobs.command("document-folders").description("List document folders for the company").option("--page-size <n>", "Number of items per page").option("--record-start-index <n>", "Index of first element to return", "0").option("--sort-order <order>", "Ascending or Descending", "Ascending").action(async (opts) => {
|
|
223
|
-
const
|
|
100
|
+
const result = await documentFolders(getClient2(), {
|
|
101
|
+
pageSize: opts.pageSize,
|
|
224
102
|
recordStartIndex: opts.recordStartIndex,
|
|
225
103
|
sortOrder: opts.sortOrder
|
|
226
|
-
};
|
|
227
|
-
if (opts.pageSize) params.pageSize = opts.pageSize;
|
|
228
|
-
const result = await getClient2().get(
|
|
229
|
-
"/company-settings/job-file-settings/document-folders",
|
|
230
|
-
params
|
|
231
|
-
);
|
|
104
|
+
});
|
|
232
105
|
console.log(JSON.stringify(result));
|
|
233
106
|
});
|
|
234
107
|
jobs.command("add-expense").argument("<jobId>", "Job ID").description("Record an additional expense on a job (pipe JSON body to stdin)").action(async (jobId) => {
|
|
235
108
|
const body = await readStdin();
|
|
236
|
-
|
|
237
|
-
`/jobs/${jobId}/payments/expense`,
|
|
238
|
-
body
|
|
239
|
-
);
|
|
240
|
-
console.log(JSON.stringify(result));
|
|
109
|
+
console.log(JSON.stringify(await jobAddExpense(getClient2(), jobId, body)));
|
|
241
110
|
});
|
|
242
111
|
jobs.command("upload-document").argument("<jobId>", "Job ID").argument("<filePath>", "Path to the file to upload").requiredOption("--folder-id <id>", "Document folder ID (required)").option("--description <text>", "Brief description of the file").option("--external-id <id>", "External reference identifier").option("--external-source <source>", "External reference source").description("Upload a document to a job").action(async (jobId, filePath, opts) => {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const file = new File([fileBuffer], fileName);
|
|
250
|
-
const form = new FormData();
|
|
251
|
-
form.append("file", file);
|
|
252
|
-
form.append("documentFolderId", opts.folderId);
|
|
253
|
-
if (opts.description) form.append("description", opts.description);
|
|
254
|
-
if (opts.externalId) form.append("externalId", opts.externalId);
|
|
255
|
-
if (opts.externalSource) form.append("externalSource", opts.externalSource);
|
|
256
|
-
const result = await getClient2().postForm(
|
|
257
|
-
`/jobs/${jobId}/documents`,
|
|
258
|
-
form
|
|
259
|
-
);
|
|
112
|
+
const result = await jobUploadDocument(getClient2(), jobId, filePath, {
|
|
113
|
+
folderId: opts.folderId,
|
|
114
|
+
description: opts.description,
|
|
115
|
+
externalId: opts.externalId,
|
|
116
|
+
externalSource: opts.externalSource
|
|
117
|
+
});
|
|
260
118
|
console.log(JSON.stringify(result));
|
|
261
119
|
});
|
|
262
120
|
}
|
|
@@ -266,36 +124,37 @@ function registerContactsCommands(parentCmd, getClient2) {
|
|
|
266
124
|
const contacts = parentCmd.command("contacts").description("Contact operations");
|
|
267
125
|
contacts.command("list").description("List contacts (paginated)").option("--limit <n>", "Max total results (default: 25)").option("--all", "Fetch all results (no limit)").action(async (opts) => {
|
|
268
126
|
const limit = opts.all ? Infinity : opts.limit ? parseInt(opts.limit, 10) : void 0;
|
|
269
|
-
|
|
270
|
-
console.log(JSON.stringify(result));
|
|
127
|
+
console.log(JSON.stringify(await contactsList(getClient2(), limit)));
|
|
271
128
|
});
|
|
272
129
|
contacts.command("get").argument("<contactId>", "Contact ID").description("Get contact details").action(async (contactId) => {
|
|
273
|
-
|
|
274
|
-
console.log(JSON.stringify(result));
|
|
130
|
+
console.log(JSON.stringify(await contactGet(getClient2(), contactId)));
|
|
275
131
|
});
|
|
276
132
|
contacts.command("create").description("Create a contact (pipe JSON body to stdin)").action(async () => {
|
|
277
133
|
const body = await readStdin();
|
|
278
|
-
|
|
279
|
-
console.log(JSON.stringify(result));
|
|
134
|
+
console.log(JSON.stringify(await contactCreate(getClient2(), body)));
|
|
280
135
|
});
|
|
281
136
|
contacts.command("search").description("Search contacts").requiredOption("--query <text>", "Search term (required)").requiredOption("--start-date <date>", "Start date (YYYY-MM-DD, required)").requiredOption("--end-date <date>", "End date (YYYY-MM-DD, required)").option("--sort-by <field>", "Sort by: CreatedDate, firstName, lastName", "lastName").option("--sort-order <order>", "Ascending or Descending", "Ascending").action(async (opts) => {
|
|
282
|
-
const result = await getClient2()
|
|
283
|
-
|
|
137
|
+
const result = await contactsSearch(getClient2(), {
|
|
138
|
+
query: opts.query,
|
|
284
139
|
startDate: opts.startDate,
|
|
285
140
|
endDate: opts.endDate,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
sortDirection: opts.sortOrder
|
|
289
|
-
}
|
|
141
|
+
sortBy: opts.sortBy,
|
|
142
|
+
sortOrder: opts.sortOrder
|
|
290
143
|
});
|
|
291
144
|
console.log(JSON.stringify(result));
|
|
292
145
|
});
|
|
293
146
|
contacts.command("emails").argument("<contactId>", "Contact ID").description("List contact email addresses").action(async (contactId) => {
|
|
294
|
-
|
|
295
|
-
console.log(JSON.stringify(result));
|
|
147
|
+
console.log(JSON.stringify(await contactEmails(getClient2(), contactId)));
|
|
296
148
|
});
|
|
297
149
|
contacts.command("phones").argument("<contactId>", "Contact ID").description("List contact phone numbers").action(async (contactId) => {
|
|
298
|
-
|
|
150
|
+
console.log(JSON.stringify(await contactPhones(getClient2(), contactId)));
|
|
151
|
+
});
|
|
152
|
+
contacts.command("phone-add").argument("<contactId>", "Contact ID").description("Add a phone number to a contact (omit --sms-opt-out to opt in to SMS)").requiredOption("--type <type>", "Phone type: Mobile, Home, Work").requiredOption("--number <number>", "Phone number").option("--sms-opt-out", "Opt this number out of SMS").action(async (contactId, opts) => {
|
|
153
|
+
const result = await contactPhoneAdd(getClient2(), contactId, {
|
|
154
|
+
type: opts.type,
|
|
155
|
+
number: opts.number,
|
|
156
|
+
smsOptOut: Boolean(opts.smsOptOut)
|
|
157
|
+
});
|
|
299
158
|
console.log(JSON.stringify(result));
|
|
300
159
|
});
|
|
301
160
|
}
|
|
@@ -305,24 +164,19 @@ function registerEstimatesCommands(parentCmd, getClient2) {
|
|
|
305
164
|
const estimates = parentCmd.command("estimates").description("Estimate operations");
|
|
306
165
|
estimates.command("list").description("List estimates (paginated)").option("--limit <n>", "Max total results (default: 25)").option("--all", "Fetch all results (no limit)").action(async (opts) => {
|
|
307
166
|
const limit = opts.all ? Infinity : opts.limit ? parseInt(opts.limit, 10) : void 0;
|
|
308
|
-
|
|
309
|
-
console.log(JSON.stringify(result));
|
|
167
|
+
console.log(JSON.stringify(await estimatesList(getClient2(), limit)));
|
|
310
168
|
});
|
|
311
169
|
estimates.command("get").argument("<estimateId>", "Estimate ID").description("Get estimate details").action(async (estimateId) => {
|
|
312
|
-
|
|
313
|
-
console.log(JSON.stringify(result));
|
|
170
|
+
console.log(JSON.stringify(await estimateGet(getClient2(), estimateId)));
|
|
314
171
|
});
|
|
315
172
|
estimates.command("sections").argument("<estimateId>", "Estimate ID").description("List sections for an estimate").action(async (estimateId) => {
|
|
316
|
-
|
|
317
|
-
console.log(JSON.stringify(result));
|
|
173
|
+
console.log(JSON.stringify(await estimateSections(getClient2(), estimateId)));
|
|
318
174
|
});
|
|
319
175
|
estimates.command("section").argument("<estimateId>", "Estimate ID").argument("<sectionId>", "Section ID").description("Get estimate section details").action(async (estimateId, sectionId) => {
|
|
320
|
-
|
|
321
|
-
console.log(JSON.stringify(result));
|
|
176
|
+
console.log(JSON.stringify(await estimateSection(getClient2(), estimateId, sectionId)));
|
|
322
177
|
});
|
|
323
178
|
estimates.command("items").argument("<estimateId>", "Estimate ID").argument("<sectionId>", "Section ID").description("List items in an estimate section").action(async (estimateId, sectionId) => {
|
|
324
|
-
|
|
325
|
-
console.log(JSON.stringify(result));
|
|
179
|
+
console.log(JSON.stringify(await estimateItems(getClient2(), estimateId, sectionId)));
|
|
326
180
|
});
|
|
327
181
|
}
|
|
328
182
|
|
|
@@ -330,18 +184,8 @@ function registerEstimatesCommands(parentCmd, getClient2) {
|
|
|
330
184
|
function registerUsersCommands(parentCmd, getClient2) {
|
|
331
185
|
const users = parentCmd.command("users").description("User operations");
|
|
332
186
|
users.command("list").description("List users (paginated)").option("--limit <n>", "Max total results (default: 25)").option("--all", "Fetch all results (no limit)").option("--search <text>", "Filter users by name or email (client-side)").action(async (opts) => {
|
|
333
|
-
if (opts.search) {
|
|
334
|
-
const all = await paginate(getClient2(), "/users", {}, Infinity);
|
|
335
|
-
const term = opts.search.toLowerCase();
|
|
336
|
-
const filtered = all.filter((u) => {
|
|
337
|
-
const fields = [u.displayName, u.firstName, u.lastName, u.email];
|
|
338
|
-
return fields.some((f) => f && f.toLowerCase().includes(term));
|
|
339
|
-
});
|
|
340
|
-
console.log(JSON.stringify(filtered));
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
187
|
const limit = opts.all ? Infinity : opts.limit ? parseInt(opts.limit, 10) : void 0;
|
|
344
|
-
const result = await
|
|
188
|
+
const result = await usersList(getClient2(), { limit, search: opts.search });
|
|
345
189
|
console.log(JSON.stringify(result));
|
|
346
190
|
});
|
|
347
191
|
}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
interface AccuLynxConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fail fast at boot if the API key is missing/unresolvable (getConfig throws).
|
|
9
|
+
* Exported with injectable deps so it can be unit-tested without booting the server.
|
|
10
|
+
*/
|
|
11
|
+
declare function bootSelfCheck(getCfg?: () => AccuLynxConfig, logFn?: (msg: string) => void, exit?: (code: number) => never): Promise<void>;
|
|
12
|
+
|
|
13
|
+
export { bootSelfCheck };
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
contactCreate,
|
|
4
|
+
contactEmails,
|
|
5
|
+
contactGet,
|
|
6
|
+
contactPhoneAdd,
|
|
7
|
+
contactPhones,
|
|
8
|
+
contactsList,
|
|
9
|
+
contactsSearch,
|
|
10
|
+
createApiClient,
|
|
11
|
+
documentFolders,
|
|
12
|
+
estimateGet,
|
|
13
|
+
estimateItems,
|
|
14
|
+
estimateSection,
|
|
15
|
+
estimateSections,
|
|
16
|
+
estimatesList,
|
|
17
|
+
getConfig,
|
|
18
|
+
jobAddExpense,
|
|
19
|
+
jobContacts,
|
|
20
|
+
jobCreate,
|
|
21
|
+
jobEstimates,
|
|
22
|
+
jobFinancials,
|
|
23
|
+
jobGet,
|
|
24
|
+
jobHistory,
|
|
25
|
+
jobInvoices,
|
|
26
|
+
jobMilestones,
|
|
27
|
+
jobPayments,
|
|
28
|
+
jobReps,
|
|
29
|
+
jobRepsAssign,
|
|
30
|
+
jobSearch,
|
|
31
|
+
jobUploadDocument,
|
|
32
|
+
jobsList,
|
|
33
|
+
ping,
|
|
34
|
+
usersList
|
|
35
|
+
} from "./chunk-FEKZ3BED.js";
|
|
36
|
+
|
|
37
|
+
// src/mcp.ts
|
|
38
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
39
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
40
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
41
|
+
import { createRequire } from "module";
|
|
42
|
+
|
|
43
|
+
// src/mcp/tools.ts
|
|
44
|
+
var LIMIT = { type: "number", description: "Max total results (default 25). Omit and set all=true to fetch everything." };
|
|
45
|
+
var ALL = { type: "boolean", description: "Fetch all results (ignores limit)." };
|
|
46
|
+
var DATE = { type: "string", description: "Date in YYYY-MM-DD format.", pattern: "^\\d{4}-\\d{2}-\\d{2}$" };
|
|
47
|
+
var BODY = { type: "object", description: "The full JSON request body for this resource.", additionalProperties: true };
|
|
48
|
+
var SORT_ORDER = { type: "string", enum: ["Ascending", "Descending"], description: "Sort direction." };
|
|
49
|
+
function ok(value) {
|
|
50
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
51
|
+
}
|
|
52
|
+
function fail(payload) {
|
|
53
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }], isError: true };
|
|
54
|
+
}
|
|
55
|
+
var str = (v) => typeof v === "string" ? v : void 0;
|
|
56
|
+
var num = (v) => {
|
|
57
|
+
if (v === void 0 || v === null) return void 0;
|
|
58
|
+
const n = Number(v);
|
|
59
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
60
|
+
};
|
|
61
|
+
var boolOf = (v) => typeof v === "boolean" ? v : void 0;
|
|
62
|
+
var resolveLimit = (args) => boolOf(args.all) ? Infinity : num(args.limit);
|
|
63
|
+
function defaultClient() {
|
|
64
|
+
return createApiClient(getConfig({}));
|
|
65
|
+
}
|
|
66
|
+
function buildTools() {
|
|
67
|
+
return [
|
|
68
|
+
{ name: "acculynx_ping", description: "Health check \u2014 verify the API key and connectivity.", inputSchema: { type: "object", properties: {}, required: [] } },
|
|
69
|
+
{ name: "acculynx_jobs_list", description: "List jobs (paginated). All filters optional.", inputSchema: { type: "object", properties: {
|
|
70
|
+
startDate: DATE,
|
|
71
|
+
endDate: DATE,
|
|
72
|
+
dateFilterType: { type: "string", description: "Date field to filter on." },
|
|
73
|
+
milestones: { type: "string", description: "Filter by milestones." },
|
|
74
|
+
sortBy: { type: "string", enum: ["CreatedDate", "MilestoneDate", "ModifiedDate"], description: "Sort field (default CreatedDate)." },
|
|
75
|
+
sortOrder: SORT_ORDER,
|
|
76
|
+
includes: { type: "string", enum: ["contact", "initialAppointment"], description: "Related data to include." },
|
|
77
|
+
assignment: { type: "string", enum: ["assigned", "unassigned"], description: "Filter by assignment status." },
|
|
78
|
+
limit: LIMIT,
|
|
79
|
+
all: ALL
|
|
80
|
+
}, required: [] } },
|
|
81
|
+
{ name: "acculynx_jobs_get", description: "Get one job by id.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
|
|
82
|
+
{ name: "acculynx_jobs_create", description: "Create a job from a JSON body.", inputSchema: { type: "object", properties: { body: BODY }, required: ["body"] } },
|
|
83
|
+
{ name: "acculynx_jobs_search", description: "Search jobs by a free-text term.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search term." } }, required: ["query"] } },
|
|
84
|
+
{ name: "acculynx_jobs_contacts", description: "List a job's contacts.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
|
|
85
|
+
{ name: "acculynx_jobs_estimates", description: "List a job's estimates.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
|
|
86
|
+
{ name: "acculynx_jobs_financials", description: "Get a job's financials.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
|
|
87
|
+
{ name: "acculynx_jobs_invoices", description: "List a job's invoices.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
|
|
88
|
+
{ name: "acculynx_jobs_milestones", description: "List a job's milestone history.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
|
|
89
|
+
{ name: "acculynx_jobs_payments", description: "List a job's payments.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
|
|
90
|
+
{ name: "acculynx_jobs_history", description: "Get a job's history.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
|
|
91
|
+
{ name: "acculynx_jobs_reps", description: "List a job's representatives.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
|
|
92
|
+
{ name: "acculynx_jobs_reps_assign", description: "Assign a representative to a job.", inputSchema: { type: "object", properties: {
|
|
93
|
+
jobId: { type: "string", description: "Job id." },
|
|
94
|
+
userId: { type: "string", description: "User id to assign." },
|
|
95
|
+
type: { type: "string", enum: ["company", "sales-owner", "ar-owner"], description: "Rep type (default company)." }
|
|
96
|
+
}, required: ["jobId", "userId"] } },
|
|
97
|
+
{ name: "acculynx_jobs_document_folders", description: "List the company's document folders.", inputSchema: { type: "object", properties: {
|
|
98
|
+
pageSize: { type: "number", description: "Items per page." },
|
|
99
|
+
recordStartIndex: { type: "number", description: "0-based start index (default 0)." },
|
|
100
|
+
sortOrder: SORT_ORDER
|
|
101
|
+
}, required: [] } },
|
|
102
|
+
{ name: "acculynx_jobs_add_expense", description: "Record an additional expense on a job from a JSON body.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." }, body: BODY }, required: ["jobId", "body"] } },
|
|
103
|
+
{ name: "acculynx_jobs_upload_document", description: "Upload a local file to a job. Blocked extensions: .exe .com .dll .msi .bat .cmd .sh .pl .vbs .py .php.", inputSchema: { type: "object", properties: {
|
|
104
|
+
jobId: { type: "string", description: "Job id." },
|
|
105
|
+
filePath: { type: "string", description: "Absolute path to a local file the server can read." },
|
|
106
|
+
folderId: { type: "string", description: "Destination document folder id (from acculynx_jobs_document_folders)." },
|
|
107
|
+
description: { type: "string", description: "Brief file description." },
|
|
108
|
+
externalId: { type: "string", description: "External reference id." },
|
|
109
|
+
externalSource: { type: "string", description: "External reference source." }
|
|
110
|
+
}, required: ["jobId", "filePath", "folderId"] } },
|
|
111
|
+
{ name: "acculynx_contacts_list", description: "List contacts (paginated).", inputSchema: { type: "object", properties: { limit: LIMIT, all: ALL }, required: [] } },
|
|
112
|
+
{ name: "acculynx_contacts_get", description: "Get one contact by id.", inputSchema: { type: "object", properties: { contactId: { type: "string", description: "Contact id." } }, required: ["contactId"] } },
|
|
113
|
+
{ name: "acculynx_contacts_create", description: "Create a contact from a JSON body.", inputSchema: { type: "object", properties: { body: BODY }, required: ["body"] } },
|
|
114
|
+
{ name: "acculynx_contacts_search", description: "Search contacts within a date range.", inputSchema: { type: "object", properties: {
|
|
115
|
+
query: { type: "string", description: "Search term." },
|
|
116
|
+
startDate: DATE,
|
|
117
|
+
endDate: DATE,
|
|
118
|
+
sortBy: { type: "string", enum: ["CreatedDate", "firstName", "lastName"], description: "Sort field (default lastName)." },
|
|
119
|
+
sortOrder: SORT_ORDER
|
|
120
|
+
}, required: ["query", "startDate", "endDate"] } },
|
|
121
|
+
{ name: "acculynx_contacts_emails", description: "List a contact's email addresses.", inputSchema: { type: "object", properties: { contactId: { type: "string", description: "Contact id." } }, required: ["contactId"] } },
|
|
122
|
+
{ name: "acculynx_contacts_phones", description: "List a contact's phone numbers.", inputSchema: { type: "object", properties: { contactId: { type: "string", description: "Contact id." } }, required: ["contactId"] } },
|
|
123
|
+
{ name: "acculynx_contacts_phone_add", description: "Add a phone number to a contact. Omit smsOptOut to opt in to SMS.", inputSchema: { type: "object", properties: {
|
|
124
|
+
contactId: { type: "string", description: "Contact id." },
|
|
125
|
+
type: { type: "string", enum: ["Mobile", "Home", "Work"], description: "Phone type." },
|
|
126
|
+
number: { type: "string", description: "Phone number." },
|
|
127
|
+
smsOptOut: { type: "boolean", description: "Opt this number OUT of SMS (default false = opted in)." }
|
|
128
|
+
}, required: ["contactId", "type", "number"] } },
|
|
129
|
+
{ name: "acculynx_estimates_list", description: "List estimates (paginated).", inputSchema: { type: "object", properties: { limit: LIMIT, all: ALL }, required: [] } },
|
|
130
|
+
{ name: "acculynx_estimates_get", description: "Get one estimate by id.", inputSchema: { type: "object", properties: { estimateId: { type: "string", description: "Estimate id." } }, required: ["estimateId"] } },
|
|
131
|
+
{ name: "acculynx_estimates_sections", description: "List an estimate's sections.", inputSchema: { type: "object", properties: { estimateId: { type: "string", description: "Estimate id." } }, required: ["estimateId"] } },
|
|
132
|
+
{ name: "acculynx_estimates_section", description: "Get one estimate section.", inputSchema: { type: "object", properties: { estimateId: { type: "string", description: "Estimate id." }, sectionId: { type: "string", description: "Section id." } }, required: ["estimateId", "sectionId"] } },
|
|
133
|
+
{ name: "acculynx_estimates_items", description: "List items in an estimate section.", inputSchema: { type: "object", properties: { estimateId: { type: "string", description: "Estimate id." }, sectionId: { type: "string", description: "Section id." } }, required: ["estimateId", "sectionId"] } },
|
|
134
|
+
{ name: "acculynx_users_list", description: "List users (paginated). Optional client-side search by name/email.", inputSchema: { type: "object", properties: { limit: LIMIT, all: ALL, search: { type: "string", description: "Filter users by name or email (case-insensitive substring)." } }, required: [] } }
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
async function handleToolCall(call, makeClient = defaultClient) {
|
|
138
|
+
const args = call.args ?? {};
|
|
139
|
+
try {
|
|
140
|
+
const client = makeClient();
|
|
141
|
+
switch (call.name) {
|
|
142
|
+
case "acculynx_ping":
|
|
143
|
+
return ok(await ping(client));
|
|
144
|
+
case "acculynx_jobs_list":
|
|
145
|
+
return ok(await jobsList(client, {
|
|
146
|
+
startDate: str(args.startDate),
|
|
147
|
+
endDate: str(args.endDate),
|
|
148
|
+
dateFilterType: str(args.dateFilterType),
|
|
149
|
+
milestones: str(args.milestones),
|
|
150
|
+
sortBy: str(args.sortBy),
|
|
151
|
+
sortOrder: str(args.sortOrder),
|
|
152
|
+
includes: str(args.includes),
|
|
153
|
+
assignment: str(args.assignment),
|
|
154
|
+
limit: resolveLimit(args)
|
|
155
|
+
}));
|
|
156
|
+
case "acculynx_jobs_get":
|
|
157
|
+
return ok(await jobGet(client, str(args.jobId) ?? ""));
|
|
158
|
+
case "acculynx_jobs_create":
|
|
159
|
+
return ok(await jobCreate(client, args.body));
|
|
160
|
+
case "acculynx_jobs_search":
|
|
161
|
+
return ok(await jobSearch(client, str(args.query) ?? ""));
|
|
162
|
+
case "acculynx_jobs_contacts":
|
|
163
|
+
return ok(await jobContacts(client, str(args.jobId) ?? ""));
|
|
164
|
+
case "acculynx_jobs_estimates":
|
|
165
|
+
return ok(await jobEstimates(client, str(args.jobId) ?? ""));
|
|
166
|
+
case "acculynx_jobs_financials":
|
|
167
|
+
return ok(await jobFinancials(client, str(args.jobId) ?? ""));
|
|
168
|
+
case "acculynx_jobs_invoices":
|
|
169
|
+
return ok(await jobInvoices(client, str(args.jobId) ?? ""));
|
|
170
|
+
case "acculynx_jobs_milestones":
|
|
171
|
+
return ok(await jobMilestones(client, str(args.jobId) ?? ""));
|
|
172
|
+
case "acculynx_jobs_payments":
|
|
173
|
+
return ok(await jobPayments(client, str(args.jobId) ?? ""));
|
|
174
|
+
case "acculynx_jobs_history":
|
|
175
|
+
return ok(await jobHistory(client, str(args.jobId) ?? ""));
|
|
176
|
+
case "acculynx_jobs_reps":
|
|
177
|
+
return ok(await jobReps(client, str(args.jobId) ?? ""));
|
|
178
|
+
case "acculynx_jobs_reps_assign":
|
|
179
|
+
return ok(await jobRepsAssign(client, str(args.jobId) ?? "", { userId: str(args.userId) ?? "", type: str(args.type) }));
|
|
180
|
+
case "acculynx_jobs_document_folders":
|
|
181
|
+
return ok(await documentFolders(client, {
|
|
182
|
+
pageSize: args.pageSize !== void 0 ? String(num(args.pageSize)) : void 0,
|
|
183
|
+
recordStartIndex: args.recordStartIndex !== void 0 ? String(num(args.recordStartIndex)) : void 0,
|
|
184
|
+
sortOrder: str(args.sortOrder)
|
|
185
|
+
}));
|
|
186
|
+
case "acculynx_jobs_add_expense":
|
|
187
|
+
return ok(await jobAddExpense(client, str(args.jobId) ?? "", args.body));
|
|
188
|
+
case "acculynx_jobs_upload_document":
|
|
189
|
+
return ok(await jobUploadDocument(client, str(args.jobId) ?? "", str(args.filePath) ?? "", {
|
|
190
|
+
folderId: str(args.folderId) ?? "",
|
|
191
|
+
description: str(args.description),
|
|
192
|
+
externalId: str(args.externalId),
|
|
193
|
+
externalSource: str(args.externalSource)
|
|
194
|
+
}));
|
|
195
|
+
case "acculynx_contacts_list":
|
|
196
|
+
return ok(await contactsList(client, resolveLimit(args)));
|
|
197
|
+
case "acculynx_contacts_get":
|
|
198
|
+
return ok(await contactGet(client, str(args.contactId) ?? ""));
|
|
199
|
+
case "acculynx_contacts_create":
|
|
200
|
+
return ok(await contactCreate(client, args.body));
|
|
201
|
+
case "acculynx_contacts_search":
|
|
202
|
+
return ok(await contactsSearch(client, {
|
|
203
|
+
query: str(args.query) ?? "",
|
|
204
|
+
startDate: str(args.startDate) ?? "",
|
|
205
|
+
endDate: str(args.endDate) ?? "",
|
|
206
|
+
sortBy: str(args.sortBy),
|
|
207
|
+
sortOrder: str(args.sortOrder)
|
|
208
|
+
}));
|
|
209
|
+
case "acculynx_contacts_emails":
|
|
210
|
+
return ok(await contactEmails(client, str(args.contactId) ?? ""));
|
|
211
|
+
case "acculynx_contacts_phones":
|
|
212
|
+
return ok(await contactPhones(client, str(args.contactId) ?? ""));
|
|
213
|
+
case "acculynx_contacts_phone_add":
|
|
214
|
+
return ok(await contactPhoneAdd(client, str(args.contactId) ?? "", {
|
|
215
|
+
type: str(args.type) ?? "",
|
|
216
|
+
number: str(args.number) ?? "",
|
|
217
|
+
smsOptOut: boolOf(args.smsOptOut)
|
|
218
|
+
}));
|
|
219
|
+
case "acculynx_estimates_list":
|
|
220
|
+
return ok(await estimatesList(client, resolveLimit(args)));
|
|
221
|
+
case "acculynx_estimates_get":
|
|
222
|
+
return ok(await estimateGet(client, str(args.estimateId) ?? ""));
|
|
223
|
+
case "acculynx_estimates_sections":
|
|
224
|
+
return ok(await estimateSections(client, str(args.estimateId) ?? ""));
|
|
225
|
+
case "acculynx_estimates_section":
|
|
226
|
+
return ok(await estimateSection(client, str(args.estimateId) ?? "", str(args.sectionId) ?? ""));
|
|
227
|
+
case "acculynx_estimates_items":
|
|
228
|
+
return ok(await estimateItems(client, str(args.estimateId) ?? "", str(args.sectionId) ?? ""));
|
|
229
|
+
case "acculynx_users_list":
|
|
230
|
+
return ok(await usersList(client, { limit: resolveLimit(args), search: str(args.search) }));
|
|
231
|
+
default:
|
|
232
|
+
return fail({ error: "unknown_tool", message: `Unknown tool: ${call.name}` });
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return fail({ error: "acculynx_error", message: e instanceof Error ? e.message : String(e) });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/mcp.ts
|
|
240
|
+
var require2 = createRequire(import.meta.url);
|
|
241
|
+
var { version } = require2("../package.json");
|
|
242
|
+
function log(msg) {
|
|
243
|
+
process.stderr.write(`[acculynx-mcp] ${msg}
|
|
244
|
+
`);
|
|
245
|
+
}
|
|
246
|
+
async function bootSelfCheck(getCfg = () => getConfig({}), logFn = log, exit = process.exit) {
|
|
247
|
+
try {
|
|
248
|
+
getCfg();
|
|
249
|
+
} catch (e) {
|
|
250
|
+
logFn(`fatal: ${e instanceof Error ? e.message : String(e)}`);
|
|
251
|
+
exit(1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function main() {
|
|
255
|
+
await bootSelfCheck();
|
|
256
|
+
let extendedTools = [];
|
|
257
|
+
let handleExtended = null;
|
|
258
|
+
try {
|
|
259
|
+
const resolved = require2.resolve("@opsrev/acculynx-cli-unofficial");
|
|
260
|
+
const mod = await import(resolved);
|
|
261
|
+
if (typeof mod.extendedTools === "function" && typeof mod.handleExtendedToolCall === "function") {
|
|
262
|
+
extendedTools = mod.extendedTools();
|
|
263
|
+
handleExtended = mod.handleExtendedToolCall;
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
const publicNames = new Set(buildTools().map((t) => t.name));
|
|
268
|
+
const server = new Server({ name: "acculynx", version }, { capabilities: { tools: {} } });
|
|
269
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...buildTools(), ...extendedTools] }));
|
|
270
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
271
|
+
const call = { name: req.params.name, args: req.params.arguments };
|
|
272
|
+
if (publicNames.has(call.name) || !handleExtended) {
|
|
273
|
+
return await handleToolCall(call);
|
|
274
|
+
}
|
|
275
|
+
return await handleExtended(call);
|
|
276
|
+
});
|
|
277
|
+
await server.connect(new StdioServerTransport());
|
|
278
|
+
log("connected (stdio)");
|
|
279
|
+
}
|
|
280
|
+
if (process.env.VITEST === void 0) {
|
|
281
|
+
main().catch((e) => {
|
|
282
|
+
log(`fatal: ${e instanceof Error ? e.stack ?? e.message : String(e)}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
export {
|
|
287
|
+
bootSelfCheck
|
|
288
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opsrev/acculynx-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "CLI for the AccuLynx roofing CRM API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"acculynx": "./dist/index.js"
|
|
7
|
+
"acculynx": "./dist/index.js",
|
|
8
|
+
"acculynx-mcp": "./dist/mcp.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
|
-
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
11
|
+
"build": "tsup src/index.ts src/mcp.ts --format esm --dts --clean",
|
|
11
12
|
"dev": "tsx --env-file=.env src/index.ts",
|
|
12
13
|
"test": "vitest run",
|
|
13
14
|
"test:watch": "vitest",
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
"dist"
|
|
29
30
|
],
|
|
30
31
|
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
31
33
|
"commander": "^14.0.3"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|