@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 CHANGED
@@ -1,104 +1,140 @@
1
- # @opsrev/acculynx-cli
1
+ # AccuLynx MCP Server & CLI
2
2
 
3
- [![CI](https://github.com/opsrev/acculynx-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/opsrev/acculynx-cli/actions/workflows/ci.yml)
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
- ```bash
10
- npm install -g @opsrev/acculynx-cli
11
- ```
12
-
13
- ## Configuration
14
-
15
- Copy the example env file and fill in your credentials:
5
+ [![CI](https://github.com/opsrev/acculynx-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/opsrev/acculynx-cli/actions/workflows/ci.yml)
16
6
 
17
- ```bash
18
- cp .env.example .env
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
- Your `.env` is gitignored and will never be committed.
37
+ Prefer a global install? Run `npm i -g acculynx-mcp`, then use
38
+ `"command": "acculynx-mcp"` with no `args`.
22
39
 
23
- | Env Var | Flag | Description |
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
- API keys are created by your AccuLynx account administrator under Account Settings.
43
+ ## Use in Cursor / other MCP clients
28
44
 
29
- ## Commands
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
- All list commands default to **25 results**. Use `--limit <n>` for a custom count or `--all` to fetch everything:
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
- ```bash
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
- ### Jobs list filters
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
- - `--start-date <YYYY-MM-DD>` -- filter by start date
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
- ### Search
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 search --query "smith"
82
- acculynx contacts search --query "smith" --sort-by LastName --sort-order Descending
124
+ acculynx contacts list --limit 10
125
+ acculynx estimates get <estimateId>
83
126
  ```
84
127
 
85
- ### Stdin commands
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
- ## Output
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 # fill in your credentials
145
+ cp .env.example .env # fill in your credentials
110
146
  npm install
111
- npm test # run unit tests
112
- npm run dev -- --help # run in dev mode (auto-loads .env)
113
- npm run build # build to dist/
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
- The `npm run dev` script automatically loads your `.env` file via Node's `--env-file` flag (Node 20.6+). Examples:
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-GAJNPAO3.js";
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-GAJNPAO3.js";
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.12.0",
4
- "description": "CLI for the AccuLynx roofing CRM API",
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
- "cli",
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",