@opsrev/acculynx-cli 1.12.0 → 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -80
- package/dist/{chunk-GAJNPAO3.js → chunk-AKUPC2EI.js} +72 -0
- package/dist/index.js +60 -1
- package/dist/mcp.js +41 -1
- package/package.json +21 -3
package/README.md
CHANGED
|
@@ -1,104 +1,140 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AccuLynx MCP Server & CLI
|
|
2
2
|
|
|
3
|
-
[
|
|
4
|
-
|
|
5
|
-
CLI for the AccuLynx roofing CRM API. Designed for AI agent consumption -- all output is JSON.
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
3
|
+
> npm: [`acculynx-mcp`](https://www.npmjs.com/package/acculynx-mcp) (MCP server) · [`@opsrev/acculynx-cli`](https://www.npmjs.com/package/@opsrev/acculynx-cli) (CLI + MCP)
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
npm install -g @opsrev/acculynx-cli
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Configuration
|
|
14
|
-
|
|
15
|
-
Copy the example env file and fill in your credentials:
|
|
5
|
+
[](https://github.com/opsrev/acculynx-cli/actions/workflows/ci.yml)
|
|
16
6
|
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
An **MCP server** for the [AccuLynx](https://www.acculynx.com/) roofing CRM —
|
|
8
|
+
bring jobs, contacts, and estimates into Claude Desktop, Cursor, or any MCP
|
|
9
|
+
client. Every tool is exposed with an enforced JSON Schema, so an AI agent can't
|
|
10
|
+
invent a parameter or call a command that doesn't exist.
|
|
11
|
+
|
|
12
|
+
- **30 tools** across jobs, contacts, estimates, and users.
|
|
13
|
+
- **`acculynx-mcp`** — the MCP server, for Claude Desktop / Cursor / any MCP client.
|
|
14
|
+
- **`@opsrev/acculynx-cli`** — the same data as a JSON CLI (and it also ships the `acculynx-mcp` server bin).
|
|
15
|
+
|
|
16
|
+
## Use in Claude Desktop
|
|
17
|
+
|
|
18
|
+
1. Get an AccuLynx API key (your AccuLynx account administrator creates it under
|
|
19
|
+
**Account Settings → API**).
|
|
20
|
+
2. Open your Claude Desktop config file:
|
|
21
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
22
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
23
|
+
3. Add the server (zero-install via `npx`):
|
|
24
|
+
|
|
25
|
+
```jsonc
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"acculynx": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "acculynx-mcp"],
|
|
31
|
+
"env": { "ACCULYNX_API_KEY": "your-api-key" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
19
35
|
```
|
|
20
36
|
|
|
21
|
-
|
|
37
|
+
Prefer a global install? Run `npm i -g acculynx-mcp`, then use
|
|
38
|
+
`"command": "acculynx-mcp"` with no `args`.
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
| `ACCULYNX_API_KEY` | `--api-key` | AccuLynx API key (required) |
|
|
40
|
+
4. Restart Claude Desktop. Open a new chat and ask *"List my 5 most recent
|
|
41
|
+
AccuLynx jobs"* — you should see the `acculynx_*` tools available.
|
|
26
42
|
|
|
27
|
-
|
|
43
|
+
## Use in Cursor / other MCP clients
|
|
28
44
|
|
|
29
|
-
|
|
45
|
+
Any client that speaks MCP over stdio works. Point it at `npx -y acculynx-mcp`
|
|
46
|
+
(or a global `acculynx-mcp`) and set `ACCULYNX_API_KEY` in the environment.
|
|
47
|
+
Example (Cursor `~/.cursor/mcp.json`):
|
|
30
48
|
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"acculynx": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["-y", "acculynx-mcp"],
|
|
55
|
+
"env": { "ACCULYNX_API_KEY": "your-api-key" }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
31
59
|
```
|
|
32
|
-
acculynx ping # Health check / verify API key
|
|
33
|
-
acculynx jobs list [--limit <n>] [--all] # List jobs (default: 25 results)
|
|
34
|
-
acculynx jobs get <jobId> # Get job details
|
|
35
|
-
acculynx jobs create # Create a job (JSON from stdin)
|
|
36
|
-
acculynx jobs search --query <text> # Search jobs
|
|
37
|
-
acculynx jobs contacts <jobId> # List job contacts
|
|
38
|
-
acculynx jobs estimates <jobId> # List job estimates
|
|
39
|
-
acculynx jobs financials <jobId> # Get job financials
|
|
40
|
-
acculynx jobs invoices <jobId> # List job invoices
|
|
41
|
-
acculynx jobs milestones <jobId> # List job milestone history
|
|
42
|
-
acculynx jobs payments <jobId> # List job payments
|
|
43
|
-
acculynx jobs history <jobId> # Get job change history
|
|
44
|
-
acculynx contacts list [--limit <n>] [--all] # List contacts (default: 25 results)
|
|
45
|
-
acculynx contacts get <contactId> # Get contact details
|
|
46
|
-
acculynx contacts create # Create a contact (JSON from stdin)
|
|
47
|
-
acculynx contacts search --query <text> # Search contacts
|
|
48
|
-
acculynx contacts emails <contactId> # List contact email addresses
|
|
49
|
-
acculynx contacts phones <contactId> # List contact phone numbers
|
|
50
|
-
acculynx estimates list [--limit <n>] [--all] # List estimates (default: 25 results)
|
|
51
|
-
acculynx estimates get <estimateId> # Get estimate details
|
|
52
|
-
acculynx estimates sections <estimateId> # List sections for an estimate
|
|
53
|
-
acculynx estimates section <estimateId> <secId> # Get section details
|
|
54
|
-
acculynx estimates items <estimateId> <secId> # List items in a section
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### Pagination
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
## MCP tools
|
|
62
|
+
|
|
63
|
+
All tools are prefixed `acculynx_` and return JSON.
|
|
64
|
+
|
|
65
|
+
| Tool | Description |
|
|
66
|
+
|------|-------------|
|
|
67
|
+
| `acculynx_ping` | Health check — verify the API key and connectivity |
|
|
68
|
+
| `acculynx_jobs_list` | List jobs (paginated, filterable) |
|
|
69
|
+
| `acculynx_jobs_get` | Get one job by id |
|
|
70
|
+
| `acculynx_jobs_create` | Create a job |
|
|
71
|
+
| `acculynx_jobs_search` | Search jobs by free-text term |
|
|
72
|
+
| `acculynx_jobs_contacts` | List a job's contacts |
|
|
73
|
+
| `acculynx_jobs_estimates` | List a job's estimates |
|
|
74
|
+
| `acculynx_jobs_financials` | Get a job's financials |
|
|
75
|
+
| `acculynx_jobs_invoices` | List a job's invoices |
|
|
76
|
+
| `acculynx_jobs_milestones` | List a job's milestone history |
|
|
77
|
+
| `acculynx_jobs_payments` | List a job's payments |
|
|
78
|
+
| `acculynx_jobs_history` | Get a job's change history |
|
|
79
|
+
| `acculynx_jobs_reps` | List a job's representatives |
|
|
80
|
+
| `acculynx_jobs_reps_assign` | Assign a representative to a job |
|
|
81
|
+
| `acculynx_jobs_work_types` | List the company's active work types (id + name) |
|
|
82
|
+
| `acculynx_jobs_trade_types` | List the company's active trade types (id + name) |
|
|
83
|
+
| `acculynx_jobs_set_work_type` | Set a job's work type — by `workType` name or `workTypeId` |
|
|
84
|
+
| `acculynx_jobs_set_trade_types` | Set a job's trade types — by `tradeTypes` names or `tradeTypeIds` (empty array unassigns all) |
|
|
85
|
+
| `acculynx_jobs_document_folders` | List the company's document folders |
|
|
86
|
+
| `acculynx_jobs_add_expense` | Record an additional expense on a job |
|
|
87
|
+
| `acculynx_jobs_upload_document` | Upload a local file to a job |
|
|
88
|
+
| `acculynx_contacts_list` | List contacts (paginated) |
|
|
89
|
+
| `acculynx_contacts_get` | Get one contact by id |
|
|
90
|
+
| `acculynx_contacts_create` | Create a contact |
|
|
91
|
+
| `acculynx_contacts_search` | Search contacts within a date range |
|
|
92
|
+
| `acculynx_contacts_emails` | List a contact's email addresses |
|
|
93
|
+
| `acculynx_contacts_phones` | List a contact's phone numbers |
|
|
94
|
+
| `acculynx_contacts_phone_add` | Add a phone number to a contact |
|
|
95
|
+
| `acculynx_estimates_list` | List estimates (paginated) |
|
|
96
|
+
| `acculynx_estimates_get` | Get one estimate by id |
|
|
97
|
+
| `acculynx_estimates_sections` | List an estimate's sections |
|
|
98
|
+
| `acculynx_estimates_section` | Get one estimate section |
|
|
99
|
+
| `acculynx_estimates_items` | List items in an estimate section |
|
|
100
|
+
| `acculynx_users_list` | List users (paginated, optional name/email search) |
|
|
60
101
|
|
|
61
|
-
|
|
62
|
-
acculynx jobs list # 25 results (default)
|
|
63
|
-
acculynx jobs list --limit 100 # 100 results
|
|
64
|
-
acculynx jobs list --all # all results
|
|
65
|
-
```
|
|
102
|
+
## Configuration
|
|
66
103
|
|
|
67
|
-
|
|
104
|
+
| Env Var | Flag | Required | Description |
|
|
105
|
+
|---------|------|----------|-------------|
|
|
106
|
+
| `ACCULYNX_API_KEY` | `--api-key` | Yes | AccuLynx official API key (Account Settings → API) |
|
|
107
|
+
| `ACCULYNX_EMAIL` | — | No | Login email; only for tools that use AccuLynx's web API |
|
|
108
|
+
| `ACCULYNX_PASSWORD` | — | No | Login password; only for web-API tools |
|
|
109
|
+
| `ACCULYNX_COMPANY_ID` | — | No | Company GUID; only for web-API tools |
|
|
68
110
|
|
|
69
|
-
|
|
70
|
-
- `--end-date <YYYY-MM-DD>` -- filter by end date
|
|
71
|
-
- `--date-filter-type <type>` -- date field to filter on (default: CreatedDate)
|
|
72
|
-
- `--milestones <milestones>` -- filter by milestones
|
|
73
|
-
- `--sort-by <field>` -- CreatedDate, MilestoneDate, or ModifiedDate
|
|
74
|
-
- `--sort-order <order>` -- Ascending or Descending
|
|
75
|
-
- `--includes <fields>` -- contact, initialAppointment
|
|
111
|
+
## Also a CLI
|
|
76
112
|
|
|
77
|
-
|
|
113
|
+
`@opsrev/acculynx-cli` exposes the same data as a JSON CLI (and bundles the
|
|
114
|
+
`acculynx-mcp` server bin). All output is JSON to stdout; errors are
|
|
115
|
+
`{"error": "..."}` to stderr with exit code 1.
|
|
78
116
|
|
|
79
117
|
```bash
|
|
118
|
+
npm install -g @opsrev/acculynx-cli
|
|
119
|
+
|
|
120
|
+
acculynx ping # verify API key
|
|
121
|
+
acculynx jobs list --limit 5 # 25 results by default; --all to fetch everything
|
|
122
|
+
acculynx jobs get <jobId>
|
|
80
123
|
acculynx jobs search --query "smith"
|
|
81
|
-
acculynx contacts
|
|
82
|
-
acculynx
|
|
124
|
+
acculynx contacts list --limit 10
|
|
125
|
+
acculynx estimates get <estimateId>
|
|
83
126
|
```
|
|
84
127
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Commands that create resources accept JSON piped via stdin:
|
|
128
|
+
List commands default to **25 results**; use `--limit <n>` or `--all`. Commands
|
|
129
|
+
that create resources read a JSON body from stdin:
|
|
88
130
|
|
|
89
131
|
```bash
|
|
90
132
|
echo '{"name": "New Job"}' | acculynx jobs create
|
|
91
133
|
```
|
|
92
134
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
All output is JSON to stdout. Errors are `{"error": "..."}` to stderr with exit code 1.
|
|
135
|
+
Pipe to `jq` for scripting:
|
|
96
136
|
|
|
97
137
|
```bash
|
|
98
|
-
# Pipe to jq for pretty-printing
|
|
99
|
-
acculynx jobs list --limit 5 | jq .
|
|
100
|
-
|
|
101
|
-
# Use in scripts
|
|
102
138
|
JOB_ID=$(acculynx jobs list --limit 1 | jq -r '.[0].id')
|
|
103
139
|
acculynx jobs get "$JOB_ID" | jq .
|
|
104
140
|
```
|
|
@@ -106,15 +142,14 @@ acculynx jobs get "$JOB_ID" | jq .
|
|
|
106
142
|
## Development
|
|
107
143
|
|
|
108
144
|
```bash
|
|
109
|
-
cp .env.example .env
|
|
145
|
+
cp .env.example .env # fill in your credentials
|
|
110
146
|
npm install
|
|
111
|
-
npm test
|
|
112
|
-
npm run dev -- --help
|
|
113
|
-
npm run build
|
|
114
|
-
./test-integration.sh # run integration tests (requires ACCULYNX_API_KEY)
|
|
147
|
+
npm test # run unit tests (mocked fetch)
|
|
148
|
+
npm run dev -- --help # run the CLI in dev mode (auto-loads .env)
|
|
149
|
+
npm run build # build both bins to dist/
|
|
115
150
|
```
|
|
116
151
|
|
|
117
|
-
|
|
152
|
+
`npm run dev` auto-loads `.env` via Node's `--env-file`:
|
|
118
153
|
|
|
119
154
|
```bash
|
|
120
155
|
npm run dev -- jobs list --limit 5
|
|
@@ -113,6 +113,22 @@ function createApiClient(config) {
|
|
|
113
113
|
const text = await response.text();
|
|
114
114
|
return text ? JSON.parse(text) : { status: response.status };
|
|
115
115
|
}
|
|
116
|
+
async function doPut(path, body) {
|
|
117
|
+
const url = `${config.baseUrl}${path}`;
|
|
118
|
+
const response = await fetchWithRetry(url, {
|
|
119
|
+
method: "PUT",
|
|
120
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
121
|
+
body: JSON.stringify(sanitizeDeep(body))
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const text2 = await response.text();
|
|
125
|
+
throw new Error(
|
|
126
|
+
`API error (${response.status} ${response.statusText}): ${text2}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const text = await response.text();
|
|
130
|
+
return text ? JSON.parse(text) : { status: response.status };
|
|
131
|
+
}
|
|
116
132
|
async function doPostForm(path, body) {
|
|
117
133
|
const url = `${config.baseUrl}${path}`;
|
|
118
134
|
const { "Content-Type": _, ...formHeaders } = headers;
|
|
@@ -133,6 +149,7 @@ function createApiClient(config) {
|
|
|
133
149
|
return {
|
|
134
150
|
get: doGet,
|
|
135
151
|
post: doPost,
|
|
152
|
+
put: doPut,
|
|
136
153
|
postForm: doPostForm
|
|
137
154
|
};
|
|
138
155
|
}
|
|
@@ -233,6 +250,55 @@ function jobRepsAssign(client, jobId, opts) {
|
|
|
233
250
|
const type = opts.type ?? "company";
|
|
234
251
|
return client.post(`/jobs/${jobId}/representatives/${type}`, { id: opts.userId });
|
|
235
252
|
}
|
|
253
|
+
function jobSetWorkType(client, jobId, workTypeId) {
|
|
254
|
+
return client.put(`/jobs/${jobId}/work-type`, { workTypeId });
|
|
255
|
+
}
|
|
256
|
+
function jobSetTradeTypes(client, jobId, tradeTypeIds) {
|
|
257
|
+
return client.put(`/jobs/${jobId}/trade-types`, { tradeTypeIds });
|
|
258
|
+
}
|
|
259
|
+
function companyWorkTypes(client) {
|
|
260
|
+
return paginate(client, "/company-settings/job-file-settings/work-types", {}, Infinity);
|
|
261
|
+
}
|
|
262
|
+
function companyTradeTypes(client) {
|
|
263
|
+
return paginate(client, "/company-settings/job-file-settings/trade-types", {}, Infinity);
|
|
264
|
+
}
|
|
265
|
+
function resolveNamed(items, value, opts) {
|
|
266
|
+
const v = value.trim();
|
|
267
|
+
const idOf = (item) => item[opts.idKey];
|
|
268
|
+
const nameOf = (item) => String(item.name ?? "");
|
|
269
|
+
if (opts.idIsNumeric) {
|
|
270
|
+
if (/^\d+$/.test(v)) {
|
|
271
|
+
const hit = items.find((item) => Number(idOf(item)) === Number(v));
|
|
272
|
+
if (hit) return idOf(hit);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
const hit = items.find((item) => String(idOf(item)) === v);
|
|
276
|
+
if (hit) return idOf(hit);
|
|
277
|
+
}
|
|
278
|
+
const lower = v.toLowerCase();
|
|
279
|
+
const exact = items.filter((item) => nameOf(item).toLowerCase() === lower);
|
|
280
|
+
if (exact.length === 1) return idOf(exact[0]);
|
|
281
|
+
const partial = items.filter((item) => nameOf(item).toLowerCase().includes(lower));
|
|
282
|
+
if (partial.length === 1) return idOf(partial[0]);
|
|
283
|
+
if (partial.length > 1) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`"${value}" matches multiple ${opts.label}s: ${partial.map(nameOf).join(", ")}. Be more specific.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
throw new Error(
|
|
289
|
+
`No ${opts.label} matches "${value}". Available ${opts.label}s: ${items.map(nameOf).join(", ")}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
async function resolveWorkTypeId(client, value) {
|
|
293
|
+
const items = await companyWorkTypes(client);
|
|
294
|
+
return resolveNamed(items, value, { label: "work type", idKey: "id", idIsNumeric: true });
|
|
295
|
+
}
|
|
296
|
+
async function resolveTradeTypeIds(client, values) {
|
|
297
|
+
const items = await companyTradeTypes(client);
|
|
298
|
+
return values.map(
|
|
299
|
+
(value) => resolveNamed(items, value, { label: "trade type", idKey: "id", idIsNumeric: false })
|
|
300
|
+
);
|
|
301
|
+
}
|
|
236
302
|
function documentFolders(client, opts = {}) {
|
|
237
303
|
const params = {
|
|
238
304
|
recordStartIndex: opts.recordStartIndex ?? "0",
|
|
@@ -332,6 +398,12 @@ export {
|
|
|
332
398
|
jobHistory,
|
|
333
399
|
jobReps,
|
|
334
400
|
jobRepsAssign,
|
|
401
|
+
jobSetWorkType,
|
|
402
|
+
jobSetTradeTypes,
|
|
403
|
+
companyWorkTypes,
|
|
404
|
+
companyTradeTypes,
|
|
405
|
+
resolveWorkTypeId,
|
|
406
|
+
resolveTradeTypeIds,
|
|
335
407
|
documentFolders,
|
|
336
408
|
jobAddExpense,
|
|
337
409
|
jobUploadDocument,
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
companyTradeTypes,
|
|
4
|
+
companyWorkTypes,
|
|
3
5
|
contactCreate,
|
|
4
6
|
contactEmails,
|
|
5
7
|
contactGet,
|
|
@@ -28,12 +30,16 @@ import {
|
|
|
28
30
|
jobReps,
|
|
29
31
|
jobRepsAssign,
|
|
30
32
|
jobSearch,
|
|
33
|
+
jobSetTradeTypes,
|
|
34
|
+
jobSetWorkType,
|
|
31
35
|
jobUploadDocument,
|
|
32
36
|
jobsList,
|
|
33
37
|
ping,
|
|
34
38
|
readStdin,
|
|
39
|
+
resolveTradeTypeIds,
|
|
40
|
+
resolveWorkTypeId,
|
|
35
41
|
usersList
|
|
36
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-AKUPC2EI.js";
|
|
37
43
|
|
|
38
44
|
// src/index.ts
|
|
39
45
|
import { createRequire } from "module";
|
|
@@ -48,6 +54,9 @@ function registerPingCommand(parentCmd, getClient2) {
|
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
// src/commands/jobs.ts
|
|
57
|
+
function collect(value, previous) {
|
|
58
|
+
return [...previous, value];
|
|
59
|
+
}
|
|
51
60
|
function registerJobsCommands(parentCmd, getClient2) {
|
|
52
61
|
const jobs = parentCmd.command("jobs").description("Job operations");
|
|
53
62
|
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) => {
|
|
@@ -96,6 +105,56 @@ function registerJobsCommands(parentCmd, getClient2) {
|
|
|
96
105
|
const result = await jobRepsAssign(getClient2(), jobId, { userId: opts.userId, type: opts.type });
|
|
97
106
|
console.log(JSON.stringify(result));
|
|
98
107
|
});
|
|
108
|
+
jobs.command("work-types").description("List the company's active work types (id + name)").action(async () => {
|
|
109
|
+
console.log(JSON.stringify(await companyWorkTypes(getClient2())));
|
|
110
|
+
});
|
|
111
|
+
jobs.command("trade-types").description("List the company's active trade types (id + name)").action(async () => {
|
|
112
|
+
console.log(JSON.stringify(await companyTradeTypes(getClient2())));
|
|
113
|
+
});
|
|
114
|
+
jobs.command("set-work-type").argument("<jobId>", "Job ID").description("Set the work type for a job (by name or id)").option("--work-type <name>", "Work type name (resolved against the company's work types)").option("--work-type-id <id>", "Work type ID (integer)").action(async (jobId, opts) => {
|
|
115
|
+
const nameGiven = opts.workType !== void 0;
|
|
116
|
+
const idGiven = opts.workTypeId !== void 0;
|
|
117
|
+
if (nameGiven && idGiven) {
|
|
118
|
+
throw new Error("Use either --work-type or --work-type-id, not both");
|
|
119
|
+
}
|
|
120
|
+
if (!nameGiven && !idGiven) {
|
|
121
|
+
throw new Error("Provide --work-type <name> or --work-type-id <id>");
|
|
122
|
+
}
|
|
123
|
+
const client = getClient2();
|
|
124
|
+
let workTypeId;
|
|
125
|
+
if (idGiven) {
|
|
126
|
+
workTypeId = Number(opts.workTypeId);
|
|
127
|
+
if (!Number.isInteger(workTypeId)) {
|
|
128
|
+
throw new Error("--work-type-id must be an integer");
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
workTypeId = await resolveWorkTypeId(client, opts.workType);
|
|
132
|
+
}
|
|
133
|
+
const result = await jobSetWorkType(client, jobId, workTypeId);
|
|
134
|
+
console.log(JSON.stringify(result));
|
|
135
|
+
});
|
|
136
|
+
jobs.command("set-trade-types").argument("<jobId>", "Job ID").description(
|
|
137
|
+
"Set trade types for a job (by name or id), replacing existing ones. Use --clear to unassign all."
|
|
138
|
+
).option("--trade-type <name>", "Trade type name, resolved against the company's trade types (repeatable)", collect, []).option("--trade-type-id <uuid>", "Trade type ID (repeatable)", collect, []).option("--clear", "Unassign all trade types").action(async (jobId, opts) => {
|
|
139
|
+
const names = opts.tradeType;
|
|
140
|
+
const ids = opts.tradeTypeId;
|
|
141
|
+
const sources = [names.length > 0, ids.length > 0, Boolean(opts.clear)].filter(Boolean).length;
|
|
142
|
+
if (opts.clear && (names.length > 0 || ids.length > 0)) {
|
|
143
|
+
throw new Error("Cannot combine --clear with --trade-type or --trade-type-id");
|
|
144
|
+
}
|
|
145
|
+
if (names.length > 0 && ids.length > 0) {
|
|
146
|
+
throw new Error("Use either --trade-type or --trade-type-id, not both");
|
|
147
|
+
}
|
|
148
|
+
if (sources === 0) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"Provide --trade-type <name>, --trade-type-id <uuid>, or --clear to unassign all trade types"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const client = getClient2();
|
|
154
|
+
const resolved = names.length > 0 ? await resolveTradeTypeIds(client, names) : ids;
|
|
155
|
+
const result = await jobSetTradeTypes(client, jobId, resolved);
|
|
156
|
+
console.log(JSON.stringify(result));
|
|
157
|
+
});
|
|
99
158
|
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) => {
|
|
100
159
|
const result = await documentFolders(getClient2(), {
|
|
101
160
|
pageSize: opts.pageSize,
|
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
companyTradeTypes,
|
|
4
|
+
companyWorkTypes,
|
|
3
5
|
contactCreate,
|
|
4
6
|
contactEmails,
|
|
5
7
|
contactGet,
|
|
@@ -28,11 +30,15 @@ import {
|
|
|
28
30
|
jobReps,
|
|
29
31
|
jobRepsAssign,
|
|
30
32
|
jobSearch,
|
|
33
|
+
jobSetTradeTypes,
|
|
34
|
+
jobSetWorkType,
|
|
31
35
|
jobUploadDocument,
|
|
32
36
|
jobsList,
|
|
33
37
|
ping,
|
|
38
|
+
resolveTradeTypeIds,
|
|
39
|
+
resolveWorkTypeId,
|
|
34
40
|
usersList
|
|
35
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-AKUPC2EI.js";
|
|
36
42
|
|
|
37
43
|
// src/mcp.ts
|
|
38
44
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -59,6 +65,7 @@ var num = (v) => {
|
|
|
59
65
|
return Number.isNaN(n) ? void 0 : n;
|
|
60
66
|
};
|
|
61
67
|
var boolOf = (v) => typeof v === "boolean" ? v : void 0;
|
|
68
|
+
var strArray = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
62
69
|
var resolveLimit = (args) => boolOf(args.all) ? Infinity : num(args.limit);
|
|
63
70
|
function defaultClient() {
|
|
64
71
|
return createApiClient(getConfig({}));
|
|
@@ -94,6 +101,18 @@ function buildTools() {
|
|
|
94
101
|
userId: { type: "string", description: "User id to assign." },
|
|
95
102
|
type: { type: "string", enum: ["company", "sales-owner", "ar-owner"], description: "Rep type (default company)." }
|
|
96
103
|
}, required: ["jobId", "userId"] } },
|
|
104
|
+
{ name: "acculynx_jobs_work_types", description: "List the company's active work types (id + name). Useful for resolving a work type name to its id.", inputSchema: { type: "object", properties: {}, required: [] } },
|
|
105
|
+
{ name: "acculynx_jobs_trade_types", description: "List the company's active trade types (id + name). Useful for resolving a trade type name to its id.", inputSchema: { type: "object", properties: {}, required: [] } },
|
|
106
|
+
{ name: "acculynx_jobs_set_work_type", description: "Set a job's work type, replacing the current one. Provide exactly one of workType (a name, resolved against the company's work types) or workTypeId.", inputSchema: { type: "object", properties: {
|
|
107
|
+
jobId: { type: "string", description: "Job id." },
|
|
108
|
+
workType: { type: "string", description: "Work type name (e.g. 'Insurance'). Resolved against the company's work types; case-insensitive, unique-substring match." },
|
|
109
|
+
workTypeId: { type: "integer", description: "Work type id (integer), if already known." }
|
|
110
|
+
}, required: ["jobId"] } },
|
|
111
|
+
{ name: "acculynx_jobs_set_trade_types", description: "Set a job's trade types, replacing any existing ones. Provide exactly one of tradeTypes (names, resolved against the company's trade types) or tradeTypeIds (uuids). An empty array unassigns all trade types.", inputSchema: { type: "object", properties: {
|
|
112
|
+
jobId: { type: "string", description: "Job id." },
|
|
113
|
+
tradeTypes: { type: "array", items: { type: "string" }, description: "Trade type names (e.g. ['Windows','Roofing']). Each is resolved against the company's trade types. An empty array unassigns all." },
|
|
114
|
+
tradeTypeIds: { type: "array", items: { type: "string" }, description: "Trade type ids (uuids), if already known. An empty array unassigns all." }
|
|
115
|
+
}, required: ["jobId"] } },
|
|
97
116
|
{ name: "acculynx_jobs_document_folders", description: "List the company's document folders.", inputSchema: { type: "object", properties: {
|
|
98
117
|
pageSize: { type: "number", description: "Items per page." },
|
|
99
118
|
recordStartIndex: { type: "number", description: "0-based start index (default 0)." },
|
|
@@ -177,6 +196,27 @@ async function handleToolCall(call, makeClient = defaultClient) {
|
|
|
177
196
|
return ok(await jobReps(client, str(args.jobId) ?? ""));
|
|
178
197
|
case "acculynx_jobs_reps_assign":
|
|
179
198
|
return ok(await jobRepsAssign(client, str(args.jobId) ?? "", { userId: str(args.userId) ?? "", type: str(args.type) }));
|
|
199
|
+
case "acculynx_jobs_work_types":
|
|
200
|
+
return ok(await companyWorkTypes(client));
|
|
201
|
+
case "acculynx_jobs_trade_types":
|
|
202
|
+
return ok(await companyTradeTypes(client));
|
|
203
|
+
case "acculynx_jobs_set_work_type": {
|
|
204
|
+
const nameGiven = args.workType !== void 0;
|
|
205
|
+
const idGiven = args.workTypeId !== void 0;
|
|
206
|
+
if (nameGiven && idGiven) throw new Error("Provide either workType or workTypeId, not both");
|
|
207
|
+
if (!nameGiven && !idGiven) throw new Error("Provide workType (name) or workTypeId");
|
|
208
|
+
const workTypeId = nameGiven ? await resolveWorkTypeId(client, str(args.workType) ?? "") : num(args.workTypeId) ?? 0;
|
|
209
|
+
return ok(await jobSetWorkType(client, str(args.jobId) ?? "", workTypeId));
|
|
210
|
+
}
|
|
211
|
+
case "acculynx_jobs_set_trade_types": {
|
|
212
|
+
const namesGiven = args.tradeTypes !== void 0;
|
|
213
|
+
const idsGiven = args.tradeTypeIds !== void 0;
|
|
214
|
+
if (namesGiven && idsGiven) throw new Error("Provide either tradeTypes or tradeTypeIds, not both");
|
|
215
|
+
if (!namesGiven && !idsGiven) throw new Error("Provide tradeTypes (names) or tradeTypeIds (an empty array unassigns all)");
|
|
216
|
+
const names = strArray(args.tradeTypes);
|
|
217
|
+
const ids = namesGiven ? names.length > 0 ? await resolveTradeTypeIds(client, names) : [] : strArray(args.tradeTypeIds);
|
|
218
|
+
return ok(await jobSetTradeTypes(client, str(args.jobId) ?? "", ids));
|
|
219
|
+
}
|
|
180
220
|
case "acculynx_jobs_document_folders":
|
|
181
221
|
return ok(await documentFolders(client, {
|
|
182
222
|
pageSize: args.pageSize !== void 0 ? String(num(args.pageSize)) : void 0,
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opsrev/acculynx-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "1.13.1",
|
|
4
|
+
"description": "AccuLynx MCP server & CLI — use the AccuLynx roofing CRM in Claude Desktop, Cursor, and any MCP client. Also a JSON CLI for scripts and AI agents.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/opsrev/acculynx-cli.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/opsrev/acculynx-cli#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/opsrev/acculynx-cli/issues"
|
|
12
|
+
},
|
|
13
|
+
"mcpName": "io.github.opsrev/acculynx",
|
|
5
14
|
"type": "module",
|
|
6
15
|
"bin": {
|
|
7
16
|
"acculynx": "./dist/index.js",
|
|
@@ -16,9 +25,18 @@
|
|
|
16
25
|
},
|
|
17
26
|
"keywords": [
|
|
18
27
|
"acculynx",
|
|
19
|
-
"
|
|
28
|
+
"acculynx-mcp",
|
|
29
|
+
"mcp",
|
|
30
|
+
"model-context-protocol",
|
|
31
|
+
"modelcontextprotocol",
|
|
32
|
+
"claude",
|
|
33
|
+
"claude-desktop",
|
|
34
|
+
"cursor",
|
|
35
|
+
"ai-agents",
|
|
20
36
|
"roofing",
|
|
37
|
+
"roofing-crm",
|
|
21
38
|
"crm",
|
|
39
|
+
"cli",
|
|
22
40
|
"api"
|
|
23
41
|
],
|
|
24
42
|
"license": "MIT",
|