node-autotask 0.31.0 → 0.33.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/MCP_TOOL_CATALOG.json +31 -12
  3. package/MCP_TOOL_MANIFEST.md +18 -19
  4. package/MCP_TOOL_OVERRIDES.json +0 -12
  5. package/N8N_MCP_MANIFEST.json +44 -18
  6. package/N8N_MCP_MANIFEST.md +15 -14
  7. package/README.md +8 -6
  8. package/capabilities.json +1579 -1414
  9. package/dist/capabilities.cjs +2 -2
  10. package/dist/capabilities.js +1 -1
  11. package/dist/{chunk-3H3RJ4LA.cjs → chunk-3LAZZT7V.cjs} +143 -143
  12. package/dist/{chunk-PCLL536B.cjs → chunk-4QN43TRP.cjs} +75 -50
  13. package/dist/{chunk-6UA544OR.js → chunk-5DS6F6GF.js} +299 -113
  14. package/dist/{chunk-RQZUV74N.js → chunk-7WNE5MPG.js} +2 -2
  15. package/dist/{chunk-INSFDXHP.js → chunk-MFDQHFGJ.js} +28 -3
  16. package/dist/{chunk-DQLEVKZS.cjs → chunk-OO67MRHN.cjs} +814 -628
  17. package/dist/{chunk-QXUN2VCJ.cjs → chunk-SP2SJJZX.cjs} +1578 -1413
  18. package/dist/{chunk-EPOHRNSX.js → chunk-VKE56TTN.js} +1578 -1413
  19. package/dist/{index-Uqj987EE.d.ts → index-BaeMHZPk.d.ts} +149 -12
  20. package/dist/{index--5jrRl4u.d.cts → index-Cq0VsfSz.d.cts} +149 -12
  21. package/dist/index.cjs +22 -22
  22. package/dist/index.d.cts +2 -2
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +4 -4
  25. package/dist/operations/index.cjs +120 -133
  26. package/dist/operations/index.d.cts +2 -2
  27. package/dist/operations/index.d.ts +2 -2
  28. package/dist/operations/index.js +3 -16
  29. package/dist/pool.cjs +9 -9
  30. package/dist/pool.d.cts +1 -1
  31. package/dist/pool.d.ts +1 -1
  32. package/dist/pool.js +4 -4
  33. package/dist/resources/index.cjs +4 -4
  34. package/dist/resources/index.d.cts +1 -1
  35. package/dist/resources/index.d.ts +1 -1
  36. package/dist/resources/index.js +3 -3
  37. package/docs/API.md +62 -37
  38. package/package.json +2 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "node-autotask n8n consumer mcp projection v1",
3
- "generatedAt": "2026-10-26T22:34:43.655Z",
3
+ "generatedAt": "2026-10-27T11:39:16.557Z",
4
4
  "generator": "scripts/project-n8n-mcp.mjs",
5
5
  "inputs": {
6
6
  "capabilities": "capabilities.json (the capability registry — the single source of truth)",
@@ -1106,9 +1106,7 @@
1106
1106
  "verb": "create",
1107
1107
  "effect": "write",
1108
1108
  "kind": "primitive",
1109
- "flags": [
1110
- "sensitive"
1111
- ],
1109
+ "flags": [],
1112
1110
  "requiredArgs": [
1113
1111
  "parentId",
1114
1112
  "data"
@@ -1135,7 +1133,7 @@
1135
1133
  "openWorldHint": true
1136
1134
  },
1137
1135
  "purpose": "Create a contact. (under a company)",
1138
- "bounds": "Dry-run first (dry_run). Sensitive.",
1136
+ "bounds": "Dry-run first (dry_run).",
1139
1137
  "dryRun": true
1140
1138
  },
1141
1139
  {
@@ -1183,8 +1181,7 @@
1183
1181
  "effect": "write",
1184
1182
  "kind": "primitive",
1185
1183
  "flags": [
1186
- "idempotent",
1187
- "sensitive"
1184
+ "idempotent"
1188
1185
  ],
1189
1186
  "requiredArgs": [
1190
1187
  "parentId",
@@ -1217,7 +1214,7 @@
1217
1214
  "openWorldHint": true
1218
1215
  },
1219
1216
  "purpose": "Update a contact. (under a company)",
1220
- "bounds": "Dry-run first (dry_run). Idempotent. Sensitive.",
1217
+ "bounds": "Dry-run first (dry_run). Idempotent.",
1221
1218
  "dryRun": true
1222
1219
  },
1223
1220
  {
@@ -1271,7 +1268,7 @@
1271
1268
  "entity": "company",
1272
1269
  "apiEntity": "companies",
1273
1270
  "effectLine": "effect=read only — zero mutating requests, ungated by design",
1274
- "description": "Read company records (mode: read — effect=read operations only; zero mutating requests, ungated by design). Registry operations for companies: count: Count company records matching a JSON filter. | fullProfile: Get everything the node knows about one company in a single call: the company record, its locations, contacts and notes, its open tickets, and its projects — every part separately reported with caps. | get: Get one Company by id. | getContext: Get a company with its primary context records in one call. | getEntityInfo: Get the Company capability metadata (canCreate/canUpdate/hasUserDefinedFields/...). | getFieldInfo: Get the Company field catalog (types, picklists, required, maxLength) — the full raw field dump (the largest read of the info trio; compact discovery: operations.fieldCatalog). | getUdfInfo: Get the Company user-defined-field catalog. | query: Query Company records with a JSON filter. | resolve: Resolve a Company from a numeric id (the direct fetch, tried first) or an exact value of its primary text field(s) (companyName — exact match only). Returns the Company record with the numeric id for get/update/delete. Other identifier forms (e.g. an email address) are NOT tried by resolve — use the resource's dedicated lookup op where one exists (see autotask_catalog). | search: Search company records by text (contains on the searchable fields). | searchByDomain: Find companies whose web address matches a domain (when no web address matches, the op falls back to matching contact emails). | searchByIdentity: Find companies by any combination of companyName / email / website identity values. | unbilledWork: One call for a company's unbilled work: its posted billing items (per source), the time behind them (joined through the company's tickets and tasks — time entries carry no companyID), the per-ticket rollup, the unposted anti-join section, ex-tax totals + hours, and the billing-vs-time warnings. Bounds: Bounded: server-side pagination — up to limit records (default 500, max 2500) in one call; 1000-page guard; hasMore flags a truncation. Bounded: limit default 25, max 500. Errors carry the SDK error contract: code, category, retryable, operation, httpStatus, suggestedAction (the nextAction directive — follow the directive), correlationId, vendorError (when the vendor returned a response body — the deep-redacted body, host-sourced text wrapped as untrusted content when untrusted-wrapping is on); credentials never appear in an error message.",
1271
+ "description": "Read company records (mode: read — effect=read operations only; zero mutating requests, ungated by design). Registry operations for companies: count: Count company records matching a JSON filter. | fullProfile: Get everything the node knows about one company in a single call: the company record, its locations, contacts and notes, its open tickets, and its projects — every part separately reported with caps. | get: Get one Company by id. | getContext: Get a company with its primary context records in one call. | getEntityInfo: Get the Company capability metadata (canCreate/canUpdate/hasUserDefinedFields/...). | getFieldInfo: Get the Company field catalog (types, picklists, required, maxLength) — the full raw field dump (the largest read of the info trio; compact discovery: operations.fieldCatalog). | getUdfInfo: Get the Company user-defined-field catalog. | prepareCrmWrite: Plan a CRM write: from partial company/contact details, find the company, its locations and the location to link a contact to, duplicate contacts, the required fields still missing (with how to get each) and the next atomic write to preview. | query: Query Company records with a JSON filter. | resolve: Resolve a Company from a numeric id (the direct fetch, tried first) or an exact value of its primary text field(s) (companyName — exact match only). Returns the Company record with the numeric id for get/update/delete. Other identifier forms (e.g. an email address) are NOT tried by resolve — use the resource's dedicated lookup op where one exists (see autotask_catalog). | search: Search company records by text (contains on the searchable fields). | searchByDomain: Find companies whose web address matches a domain (when no web address matches, the op falls back to matching contact emails). | searchByIdentity: Find companies by any combination of companyName / email / website identity values. | unbilledWork: One call for a company's unbilled work: its posted billing items (per source), the time behind them (joined through the company's tickets and tasks — time entries carry no companyID), the per-ticket rollup, the unposted anti-join section, ex-tax totals + hours, and the billing-vs-time warnings. Bounds: Bounded: server-side pagination — up to limit records (default 500, max 2500) in one call; 1000-page guard; hasMore flags a truncation. Bounded: limit default 25, max 500. Errors carry the SDK error contract: code, category, retryable, operation, httpStatus, suggestedAction (the nextAction directive — follow the directive), correlationId, vendorError (when the vendor returned a response body — the deep-redacted body, host-sourced text wrapped as untrusted content when untrusted-wrapping is on); credentials never appear in an error message.",
1275
1272
  "annotations": {
1276
1273
  "readOnlyHint": true,
1277
1274
  "destructiveHint": false,
@@ -1452,6 +1449,34 @@
1452
1449
  "purpose": "Get the Company user-defined-field catalog.",
1453
1450
  "bounds": ""
1454
1451
  },
1452
+ {
1453
+ "op": "companies.prepareCrmWrite",
1454
+ "verb": "prepareCrmWrite",
1455
+ "effect": "read",
1456
+ "kind": "helper",
1457
+ "flags": [],
1458
+ "requiredArgs": [
1459
+ "input"
1460
+ ],
1461
+ "input": {
1462
+ "properties": {
1463
+ "input": {
1464
+ "type": "object — CrmPrepareInput ({ intent: create_company|add_contact|update_contact|update_company, company?: {...}, contact?: {...} })"
1465
+ }
1466
+ },
1467
+ "required": [
1468
+ "input"
1469
+ ]
1470
+ },
1471
+ "annotations": {
1472
+ "readOnlyHint": true,
1473
+ "destructiveHint": false,
1474
+ "idempotentHint": true,
1475
+ "openWorldHint": true
1476
+ },
1477
+ "purpose": "Plan a CRM write: from partial company/contact details, find the company, its locations and the location to link a contact to, duplicate contacts, the required fields still missing (with how to get each) and the next atomic write to preview.",
1478
+ "bounds": ""
1479
+ },
1455
1480
  {
1456
1481
  "op": "companies.query",
1457
1482
  "verb": "query",
@@ -1640,6 +1665,7 @@
1640
1665
  "companies.getEntityInfo",
1641
1666
  "companies.getFieldInfo",
1642
1667
  "companies.getUdfInfo",
1668
+ "companies.prepareCrmWrite",
1643
1669
  "companies.query",
1644
1670
  "companies.resolve",
1645
1671
  "companies.search",
@@ -2939,7 +2965,7 @@
2939
2965
  "entity": "timeEntry",
2940
2966
  "apiEntity": "time_entries",
2941
2967
  "effectLine": "effect=read only — zero mutating requests, ungated by design",
2942
- "description": "Read timeEntry records (mode: read — effect=read operations only; zero mutating requests, ungated by design). Registry operations for time_entries: count: Count time entry records matching a JSON filter. | customRange: Get time entries for an explicit dateWorked range. | dateWorkedWindow: The unambiguous-named alias for time_entries.postedWithin (issue #66): the dateWorked window read with the posted/unposted split, counted in one pass — same wire, same result (incl. truncated). The window is ON dateWorked (entries worked within [from, to]); \"postedWithin\" keeps its historical name (its \"posted\" is the posted/unposted split over the returned rows, never a posted-date window). | get: Get one TimeEntry by id. | getEntityInfo: Get the TimeEntry capability metadata (canCreate/canUpdate/hasUserDefinedFields/...). | getFieldInfo: Get the TimeEntry field catalog (types, picklists, required, maxLength) — the full raw field dump (the largest read of the info trio; compact discovery: operations.fieldCatalog). | getPosted: Get posted time entries (optionally for one ticket). | getUdfInfo: Get the TimeEntry user-defined-field catalog. | getUnposted: Get unposted time entries (optionally for one ticket). | last14: Get time entries for the last 14 days (rolling, dateWorked). | last24h: Get time entries for the last 24 hours (rolling, dateWorked). | last30: Get time entries for the last 30 days (rolling, dateWorked). | last7: Get time entries for the last 7 days (rolling, dateWorked). | last90: Get time entries for the last 90 days (rolling, dateWorked). | lastFullFortnight: Get time entries for the last complete fortnight. | lastFullMonth: Get time entries for the last complete calendar month. | lastFullQuarter: Get time entries for the last complete calendar quarter. | lastFullWeek: Get time entries for the last complete calendar week. | postedWithin: Get time entries WORKED within an inclusive dateWorked window with the posted/unposted split, counted in one pass. The window is ON the dateWorked field — the name's \"posted\" is the posted/unposted SPLIT counted over the returned rows (the server-side postedDate exist|notExist predicate), never a posted-date window: no postedDate window op exists (the vendored TimeEntryModel declares no postedDate filter, and ww29-class zones 500 on postedDate filters ONLY WHEN the `posted` narrowing is set — omit `posted` on such zones — W-D3). The counts cover the returned rows only; truncated flags the capped window. time_entries.dateWorkedWindow is the unambiguous name. | query: Query TimeEntry records with a JSON filter. | recentTickets: List the distinct tickets a resource logged time against in an inclusive dateWorked window, with entry count, total hours, last worked date and the ticket's number, title, status, assignee and company. | resolve: Resolve a time entry from a numeric id (the direct fetch) only — this resource has no searchable text field, so a non-numeric identifier throws a config error. | today: Get time entries for today (dateWorked = now, server date). | yesterday: Get time entries for yesterday (dateWorked = yesterday, server date).. Bounds: Bounded: server-side pagination — up to limit records (default 500, max 2500) in one call; 1000-page guard; hasMore flags a truncation. Errors carry the SDK error contract: code, category, retryable, operation, httpStatus, suggestedAction (the nextAction directive — follow the directive), correlationId, vendorError (when the vendor returned a response body — the deep-redacted body, host-sourced text wrapped as untrusted content when untrusted-wrapping is on); credentials never appear in an error message.",
2968
+ "description": "Read timeEntry records (mode: read — effect=read operations only; zero mutating requests, ungated by design). Registry operations for time_entries: count: Count time entry records matching a JSON filter. | customRange: Get time entries for an explicit dateWorked range. | dateWorkedWindow: The unambiguous-named alias for time_entries.postedWithin (issue #66): the dateWorked window read with the posted/unposted split derived from the billing_items.timeEntryID join — same wire, same result (incl. truncated). The window is ON dateWorked (entries worked within [from, to]); \"postedWithin\" keeps its historical name (its \"posted\" is the posted/unposted split, never a posted-date window or a postedDate read). | get: Get one TimeEntry by id. | getEntityInfo: Get the TimeEntry capability metadata (canCreate/canUpdate/hasUserDefinedFields/...). | getFieldInfo: Get the TimeEntry field catalog (types, picklists, required, maxLength) — the full raw field dump (the largest read of the info trio; compact discovery: operations.fieldCatalog). | getPosted: Get posted time entries (an entry that a billing item references), optionally for one ticket and/or a dateWorked window — posted state derives from the billing_items.timeEntryID join, never a postedDate read or approval. | getUdfInfo: Get the TimeEntry user-defined-field catalog. | getUnposted: Get unposted time entries (entries no billing item references), optionally for one ticket and/or a dateWorked window — posted state derives from the billing_items.timeEntryID join, never a postedDate read or approval. | last14: Get time entries for the last 14 days (rolling, dateWorked). | last24h: Get time entries for the last 24 hours (rolling, dateWorked). | last30: Get time entries for the last 30 days (rolling, dateWorked). | last7: Get time entries for the last 7 days (rolling, dateWorked). | last90: Get time entries for the last 90 days (rolling, dateWorked). | lastFullFortnight: Get time entries for the last complete fortnight. | lastFullMonth: Get time entries for the last complete calendar month. | lastFullQuarter: Get time entries for the last complete calendar quarter. | lastFullWeek: Get time entries for the last complete calendar week. | postedWithin: Get time entries WORKED within an inclusive dateWorked window with the posted/unposted split. The window is ON the dateWorked field — the name's \"posted\" is the posted/unposted SPLIT derived from the billing_items.timeEntryID join (an entry a billing item references is posted, approval is not consulted), never a posted-date window or a postedDate read. The counts cover the returned rows only; truncated flags a capped read. time_entries.dateWorkedWindow is the unambiguous name. | query: Query TimeEntry records with a JSON filter. | recentTickets: List the distinct tickets a resource logged time against in an inclusive dateWorked window, with entry count, total hours, last worked date and the ticket's number, title, status, assignee and company. | resolve: Resolve a time entry from a numeric id (the direct fetch) only — this resource has no searchable text field, so a non-numeric identifier throws a config error. | today: Get time entries for today (dateWorked = now, server date). | yesterday: Get time entries for yesterday (dateWorked = yesterday, server date).. Bounds: Bounded: server-side pagination — up to limit records (default 500, max 2500) in one call; 1000-page guard; hasMore flags a truncation. Errors carry the SDK error contract: code, category, retryable, operation, httpStatus, suggestedAction (the nextAction directive — follow the directive), correlationId, vendorError (when the vendor returned a response body — the deep-redacted body, host-sourced text wrapped as untrusted content when untrusted-wrapping is on); credentials never appear in an error message.",
2943
2969
  "annotations": {
2944
2970
  "readOnlyHint": true,
2945
2971
  "destructiveHint": false,
@@ -3044,7 +3070,7 @@
3044
3070
  "idempotentHint": true,
3045
3071
  "openWorldHint": true
3046
3072
  },
3047
- "purpose": "The unambiguous-named alias for time_entries.postedWithin (issue #66): the dateWorked window read with the posted/unposted split, counted in one pass — same wire, same result (incl. truncated). The window is ON dateWorked (entries worked within [from, to]); \"postedWithin\" keeps its historical name (its \"posted\" is the posted/unposted split over the returned rows, never a posted-date window).",
3073
+ "purpose": "The unambiguous-named alias for time_entries.postedWithin (issue #66): the dateWorked window read with the posted/unposted split derived from the billing_items.timeEntryID join — same wire, same result (incl. truncated). The window is ON dateWorked (entries worked within [from, to]); \"postedWithin\" keeps its historical name (its \"posted\" is the posted/unposted split, never a posted-date window or a postedDate read).",
3048
3074
  "bounds": ""
3049
3075
  },
3050
3076
  {
@@ -3132,7 +3158,7 @@
3132
3158
  "idempotentHint": true,
3133
3159
  "openWorldHint": true
3134
3160
  },
3135
- "purpose": "Get posted time entries (optionally for one ticket).",
3161
+ "purpose": "Get posted time entries (an entry that a billing item references), optionally for one ticket and/or a dateWorked window — posted state derives from the billing_items.timeEntryID join, never a postedDate read or approval.",
3136
3162
  "bounds": ""
3137
3163
  },
3138
3164
  {
@@ -3172,7 +3198,7 @@
3172
3198
  "idempotentHint": true,
3173
3199
  "openWorldHint": true
3174
3200
  },
3175
- "purpose": "Get unposted time entries (optionally for one ticket).",
3201
+ "purpose": "Get unposted time entries (entries no billing item references), optionally for one ticket and/or a dateWorked window — posted state derives from the billing_items.timeEntryID join, never a postedDate read or approval.",
3176
3202
  "bounds": ""
3177
3203
  },
3178
3204
  {
@@ -3371,7 +3397,7 @@
3371
3397
  "type": "string (UTC date, inclusive — the dateWorked window start: the from-day start) — opts.tz never shifts it (dateWorked is date-only)"
3372
3398
  },
3373
3399
  "to": {
3374
- "type": "string (UTC date, inclusive — the dateWorked window end: the to-day end) — opts.tz never shifts it (dateWorked is date-only). ONE server-filtered query; the window is ON the dateWorked field (the posted narrowing is a server-side postedDate exist|notExist predicate). Result { items, postedCount, unpostedCount, window, truncated } — the counts cover the returned rows only (at most limit); truncated is true when the vendor returned a live next-page cursor (more matching rows exist)"
3400
+ "type": "string (UTC date, inclusive — the dateWorked window end: the to-day end) — opts.tz never shifts it (dateWorked is date-only). One bounded dateWorked read (max 500 entries) plus at most 2 billing_items joins (250 ids each, every read entry); the window is ON the dateWorked field (the posted narrowing is a client-side filter after the join). Result { items, postedCount, unpostedCount, window, truncated } — the counts cover the returned rows only (at most limit); truncated is true when the entry read has a live next-page cursor (more matching rows exist) or a join page was truncated"
3375
3401
  }
3376
3402
  },
3377
3403
  "required": [
@@ -3385,7 +3411,7 @@
3385
3411
  "idempotentHint": true,
3386
3412
  "openWorldHint": true
3387
3413
  },
3388
- "purpose": "Get time entries WORKED within an inclusive dateWorked window with the posted/unposted split, counted in one pass. The window is ON the dateWorked field — the name's \"posted\" is the posted/unposted SPLIT counted over the returned rows (the server-side postedDate exist|notExist predicate), never a posted-date window: no postedDate window op exists (the vendored TimeEntryModel declares no postedDate filter, and ww29-class zones 500 on postedDate filters ONLY WHEN the `posted` narrowing is set — omit `posted` on such zones — W-D3). The counts cover the returned rows only; truncated flags the capped window. time_entries.dateWorkedWindow is the unambiguous name.",
3414
+ "purpose": "Get time entries WORKED within an inclusive dateWorked window with the posted/unposted split. The window is ON the dateWorked field — the name's \"posted\" is the posted/unposted SPLIT derived from the billing_items.timeEntryID join (an entry a billing item references is posted, approval is not consulted), never a posted-date window or a postedDate read. The counts cover the returned rows only; truncated flags a capped read. time_entries.dateWorkedWindow is the unambiguous name.",
3389
3415
  "bounds": ""
3390
3416
  },
3391
3417
  {
@@ -3856,17 +3882,17 @@
3856
3882
  }
3857
3883
  ],
3858
3884
  "stats": {
3859
- "registryRecords": 1279,
3885
+ "registryRecords": 1280,
3860
3886
  "curatedEntities": 7,
3861
3887
  "includedEntities": 7,
3862
3888
  "readTools": 7,
3863
3889
  "writeTools": 7,
3864
3890
  "totalTools": 14,
3865
- "readOperations": 88,
3891
+ "readOperations": 89,
3866
3892
  "writeOperations": 22,
3867
3893
  "approvalGatedWrites": 2,
3868
3894
  "destructiveExcluded": 4
3869
3895
  },
3870
- "capabilitiesHash": "68a3fbb3ceb8cf046bd966f47b7526050baa536df8199ddb21828582fcd69ff1",
3896
+ "capabilitiesHash": "ba97b65fc4424ba19d524a07a92892920d809ab6aa37fc7c79d849725e73861b",
3871
3897
  "curationHash": "e19117ce73e38d9aded3be7e7f077a4e185d2e10294622b76c17d67f63cf9e3b"
3872
3898
  }
@@ -1,6 +1,6 @@
1
1
  # N8N MCP Manifest — node-autotask (consumer projection)
2
2
 
3
- <!-- GENERATED by scripts/project-n8n-mcp.mjs at 2026-10-26T22:34:43.655Z — do not hand-edit. -->
3
+ <!-- GENERATED by scripts/project-n8n-mcp.mjs at 2026-10-27T11:39:16.557Z — do not hand-edit. -->
4
4
 
5
5
  The n8n CONSUMER projection of the capability registry: the canonical source for what the
6
6
  n8n-side MCP tools must expose. The SDK is canonical — this is a projection of it, never a
@@ -27,7 +27,7 @@ user's n8n instances; PROD is read-only for any verification).
27
27
  | `autotask_ticket_write` | write | ticket | false | false | false | true | 4 |
28
28
  | `autotask_contact` | read | contact | true | false | true | true | 10 |
29
29
  | `autotask_contact_write` | write | contact | false | false | false | true | 4 |
30
- | `autotask_company` | read | company | true | false | true | true | 13 |
30
+ | `autotask_company` | read | company | true | false | true | true | 14 |
31
31
  | `autotask_company_write` | write | company | false | false | false | true | 3 |
32
32
  | `autotask_companyLocation` | read | companyLocation | true | false | true | true | 8 |
33
33
  | `autotask_companyLocation_write` | write | companyLocation | false | false | false | true | 2 |
@@ -100,14 +100,14 @@ Tool input: {"operation":{"type":"string","enum":["create","moveToCompany","upda
100
100
 
101
101
  | backingOperation | verb | effect | flags | requiredArgs | purpose |
102
102
  |------------------|------|--------|-------|--------------|---------|
103
- | `contacts.create` | create | write | sensitive | parentId, data | Create a contact. (under a company) Dry-run first (dry_run). Sensitive. |
103
+ | `contacts.create` | create | write | — | parentId, data | Create a contact. (under a company) Dry-run first (dry_run). |
104
104
  | `contacts.moveToCompany` | moveToCompany | write | requiresApproval+sensitive | sourceContactId, destinationCompanyId | Move a contact to a different company. Dry-run first (dry_run). Requires confirmation (confirm: contacts.moveToCompany). Sensitive. |
105
- | `contacts.update` | update | write | idempotent+sensitive | parentId, id, data | Update a contact. (under a company) Dry-run first (dry_run). Idempotent. Sensitive. |
105
+ | `contacts.update` | update | write | idempotent | parentId, id, data | Update a contact. (under a company) Dry-run first (dry_run). Idempotent. |
106
106
  | `contacts.upsertByEmail` | upsertByEmail | write | — | companyID, email | Make sure a contact with the given email exists under the given company — find-or-create under the RIGHT parent, with the typed EXISTS/CREATED result (+ the record). Dry-run first (dry_run). |
107
107
 
108
108
  ### `autotask_company` (read — company)
109
109
 
110
- > Read company records (mode: read — effect=read operations only; zero mutating requests, ungated by design). Registry operations for companies: count: Count company records matching a JSON filter. | fullProfile: Get everything the node knows about one company in a single call: the company record, its locations, contacts and notes, its open tickets, and its projects — every part separately reported with caps. | get: Get one Company by id. | getContext: Get a company with its primary context records in one call. | getEntityInfo: Get the Company capability metadata (canCreate/canUpdate/hasUserDefinedFields/...). | getFieldInfo: Get the Company field catalog (types, picklists, required, maxLength) — the full raw field dump (the largest read of the info trio; compact discovery: operations.fieldCatalog). | getUdfInfo: Get the Company user-defined-field catalog. | query: Query Company records with a JSON filter. | resolve: Resolve a Company from a numeric id (the direct fetch, tried first) or an exact value of its primary text field(s) (companyName — exact match only). Returns the Company record with the numeric id for get/update/delete. Other identifier forms (e.g. an email address) are NOT tried by resolve — use the resource's dedicated lookup op where one exists (see autotask_catalog). | search: Search company records by text (contains on the searchable fields). | searchByDomain: Find companies whose web address matches a domain (when no web address matches, the op falls back to matching contact emails). | searchByIdentity: Find companies by any combination of companyName / email / website identity values. | unbilledWork: One call for a company's unbilled work: its posted billing items (per source), the time behind them (joined through the company's tickets and tasks — time entries carry no companyID), the per-ticket rollup, the unposted anti-join section, ex-tax totals + hours, and the billing-vs-time warnings. Bounds: Bounded: server-side pagination — up to limit records (default 500, max 2500) in one call; 1000-page guard; hasMore flags a truncation. Bounded: limit default 25, max 500. Errors carry the SDK error contract: code, category, retryable, operation, httpStatus, suggestedAction (the nextAction directive — follow the directive), correlationId, vendorError (when the vendor returned a response body — the deep-redacted body, host-sourced text wrapped as untrusted content when untrusted-wrapping is on); credentials never appear in an error message.
110
+ > Read company records (mode: read — effect=read operations only; zero mutating requests, ungated by design). Registry operations for companies: count: Count company records matching a JSON filter. | fullProfile: Get everything the node knows about one company in a single call: the company record, its locations, contacts and notes, its open tickets, and its projects — every part separately reported with caps. | get: Get one Company by id. | getContext: Get a company with its primary context records in one call. | getEntityInfo: Get the Company capability metadata (canCreate/canUpdate/hasUserDefinedFields/...). | getFieldInfo: Get the Company field catalog (types, picklists, required, maxLength) — the full raw field dump (the largest read of the info trio; compact discovery: operations.fieldCatalog). | getUdfInfo: Get the Company user-defined-field catalog. | prepareCrmWrite: Plan a CRM write: from partial company/contact details, find the company, its locations and the location to link a contact to, duplicate contacts, the required fields still missing (with how to get each) and the next atomic write to preview. | query: Query Company records with a JSON filter. | resolve: Resolve a Company from a numeric id (the direct fetch, tried first) or an exact value of its primary text field(s) (companyName — exact match only). Returns the Company record with the numeric id for get/update/delete. Other identifier forms (e.g. an email address) are NOT tried by resolve — use the resource's dedicated lookup op where one exists (see autotask_catalog). | search: Search company records by text (contains on the searchable fields). | searchByDomain: Find companies whose web address matches a domain (when no web address matches, the op falls back to matching contact emails). | searchByIdentity: Find companies by any combination of companyName / email / website identity values. | unbilledWork: One call for a company's unbilled work: its posted billing items (per source), the time behind them (joined through the company's tickets and tasks — time entries carry no companyID), the per-ticket rollup, the unposted anti-join section, ex-tax totals + hours, and the billing-vs-time warnings. Bounds: Bounded: server-side pagination — up to limit records (default 500, max 2500) in one call; 1000-page guard; hasMore flags a truncation. Bounded: limit default 25, max 500. Errors carry the SDK error contract: code, category, retryable, operation, httpStatus, suggestedAction (the nextAction directive — follow the directive), correlationId, vendorError (when the vendor returned a response body — the deep-redacted body, host-sourced text wrapped as untrusted content when untrusted-wrapping is on); credentials never appear in an error message.
111
111
 
112
112
  | backingOperation | verb | effect | flags | requiredArgs | purpose |
113
113
  |------------------|------|--------|-------|--------------|---------|
@@ -118,6 +118,7 @@ Tool input: {"operation":{"type":"string","enum":["create","moveToCompany","upda
118
118
  | `companies.getEntityInfo` | getEntityInfo | read | — | none | Get the Company capability metadata (canCreate/canUpdate/hasUserDefinedFields/...). |
119
119
  | `companies.getFieldInfo` | getFieldInfo | read | — | none | Get the Company field catalog (types, picklists, required, maxLength) — the full raw field dump (the largest read of the info trio; compact discovery: operations.fieldCatalog). |
120
120
  | `companies.getUdfInfo` | getUdfInfo | read | — | none | Get the Company user-defined-field catalog. |
121
+ | `companies.prepareCrmWrite` | prepareCrmWrite | read | — | input | Plan a CRM write: from partial company/contact details, find the company, its locations and the location to link a contact to, duplicate contacts, the required fields still missing (with how to get each) and the next atomic write to preview. |
121
122
  | `companies.query` | query | read | — | none | Query Company records with a JSON filter. Bounded: server-side pagination — up to limit records (default 500, max 2500) in one call; 1000-page guard; hasMore flags a truncation. |
122
123
  | `companies.resolve` | resolve | read | — | identifier | Resolve a Company from a numeric id (the direct fetch, tried first) or an exact value of its primary text field(s) (companyName — exact match only). Returns the Company record with the numeric id for get/update/delete. Other identifier forms (e.g. an email address) are NOT tried by resolve — use the resource's dedicated lookup op where one exists (see autotask_catalog). |
123
124
  | `companies.search` | search | read | — | query | Search company records by text (contains on the searchable fields). Bounded: limit default 25, max 500. |
@@ -217,19 +218,19 @@ Tool input: {"operation":{"type":"string","enum":["transferOwnership","update"]}
217
218
 
218
219
  ### `autotask_timeEntry` (read — timeEntry)
219
220
 
220
- > Read timeEntry records (mode: read — effect=read operations only; zero mutating requests, ungated by design). Registry operations for time_entries: count: Count time entry records matching a JSON filter. | customRange: Get time entries for an explicit dateWorked range. | dateWorkedWindow: The unambiguous-named alias for time_entries.postedWithin (issue #66): the dateWorked window read with the posted/unposted split, counted in one pass — same wire, same result (incl. truncated). The window is ON dateWorked (entries worked within [from, to]); "postedWithin" keeps its historical name (its "posted" is the posted/unposted split over the returned rows, never a posted-date window). | get: Get one TimeEntry by id. | getEntityInfo: Get the TimeEntry capability metadata (canCreate/canUpdate/hasUserDefinedFields/...). | getFieldInfo: Get the TimeEntry field catalog (types, picklists, required, maxLength) — the full raw field dump (the largest read of the info trio; compact discovery: operations.fieldCatalog). | getPosted: Get posted time entries (optionally for one ticket). | getUdfInfo: Get the TimeEntry user-defined-field catalog. | getUnposted: Get unposted time entries (optionally for one ticket). | last14: Get time entries for the last 14 days (rolling, dateWorked). | last24h: Get time entries for the last 24 hours (rolling, dateWorked). | last30: Get time entries for the last 30 days (rolling, dateWorked). | last7: Get time entries for the last 7 days (rolling, dateWorked). | last90: Get time entries for the last 90 days (rolling, dateWorked). | lastFullFortnight: Get time entries for the last complete fortnight. | lastFullMonth: Get time entries for the last complete calendar month. | lastFullQuarter: Get time entries for the last complete calendar quarter. | lastFullWeek: Get time entries for the last complete calendar week. | postedWithin: Get time entries WORKED within an inclusive dateWorked window with the posted/unposted split, counted in one pass. The window is ON the dateWorked field — the name's "posted" is the posted/unposted SPLIT counted over the returned rows (the server-side postedDate exist|notExist predicate), never a posted-date window: no postedDate window op exists (the vendored TimeEntryModel declares no postedDate filter, and ww29-class zones 500 on postedDate filters ONLY WHEN the `posted` narrowing is set — omit `posted` on such zones — W-D3). The counts cover the returned rows only; truncated flags the capped window. time_entries.dateWorkedWindow is the unambiguous name. | query: Query TimeEntry records with a JSON filter. | recentTickets: List the distinct tickets a resource logged time against in an inclusive dateWorked window, with entry count, total hours, last worked date and the ticket's number, title, status, assignee and company. | resolve: Resolve a time entry from a numeric id (the direct fetch) only — this resource has no searchable text field, so a non-numeric identifier throws a config error. | today: Get time entries for today (dateWorked = now, server date). | yesterday: Get time entries for yesterday (dateWorked = yesterday, server date).. Bounds: Bounded: server-side pagination — up to limit records (default 500, max 2500) in one call; 1000-page guard; hasMore flags a truncation. Errors carry the SDK error contract: code, category, retryable, operation, httpStatus, suggestedAction (the nextAction directive — follow the directive), correlationId, vendorError (when the vendor returned a response body — the deep-redacted body, host-sourced text wrapped as untrusted content when untrusted-wrapping is on); credentials never appear in an error message.
221
+ > Read timeEntry records (mode: read — effect=read operations only; zero mutating requests, ungated by design). Registry operations for time_entries: count: Count time entry records matching a JSON filter. | customRange: Get time entries for an explicit dateWorked range. | dateWorkedWindow: The unambiguous-named alias for time_entries.postedWithin (issue #66): the dateWorked window read with the posted/unposted split derived from the billing_items.timeEntryID join — same wire, same result (incl. truncated). The window is ON dateWorked (entries worked within [from, to]); "postedWithin" keeps its historical name (its "posted" is the posted/unposted split, never a posted-date window or a postedDate read). | get: Get one TimeEntry by id. | getEntityInfo: Get the TimeEntry capability metadata (canCreate/canUpdate/hasUserDefinedFields/...). | getFieldInfo: Get the TimeEntry field catalog (types, picklists, required, maxLength) — the full raw field dump (the largest read of the info trio; compact discovery: operations.fieldCatalog). | getPosted: Get posted time entries (an entry that a billing item references), optionally for one ticket and/or a dateWorked window — posted state derives from the billing_items.timeEntryID join, never a postedDate read or approval. | getUdfInfo: Get the TimeEntry user-defined-field catalog. | getUnposted: Get unposted time entries (entries no billing item references), optionally for one ticket and/or a dateWorked window — posted state derives from the billing_items.timeEntryID join, never a postedDate read or approval. | last14: Get time entries for the last 14 days (rolling, dateWorked). | last24h: Get time entries for the last 24 hours (rolling, dateWorked). | last30: Get time entries for the last 30 days (rolling, dateWorked). | last7: Get time entries for the last 7 days (rolling, dateWorked). | last90: Get time entries for the last 90 days (rolling, dateWorked). | lastFullFortnight: Get time entries for the last complete fortnight. | lastFullMonth: Get time entries for the last complete calendar month. | lastFullQuarter: Get time entries for the last complete calendar quarter. | lastFullWeek: Get time entries for the last complete calendar week. | postedWithin: Get time entries WORKED within an inclusive dateWorked window with the posted/unposted split. The window is ON the dateWorked field — the name's "posted" is the posted/unposted SPLIT derived from the billing_items.timeEntryID join (an entry a billing item references is posted, approval is not consulted), never a posted-date window or a postedDate read. The counts cover the returned rows only; truncated flags a capped read. time_entries.dateWorkedWindow is the unambiguous name. | query: Query TimeEntry records with a JSON filter. | recentTickets: List the distinct tickets a resource logged time against in an inclusive dateWorked window, with entry count, total hours, last worked date and the ticket's number, title, status, assignee and company. | resolve: Resolve a time entry from a numeric id (the direct fetch) only — this resource has no searchable text field, so a non-numeric identifier throws a config error. | today: Get time entries for today (dateWorked = now, server date). | yesterday: Get time entries for yesterday (dateWorked = yesterday, server date).. Bounds: Bounded: server-side pagination — up to limit records (default 500, max 2500) in one call; 1000-page guard; hasMore flags a truncation. Errors carry the SDK error contract: code, category, retryable, operation, httpStatus, suggestedAction (the nextAction directive — follow the directive), correlationId, vendorError (when the vendor returned a response body — the deep-redacted body, host-sourced text wrapped as untrusted content when untrusted-wrapping is on); credentials never appear in an error message.
221
222
 
222
223
  | backingOperation | verb | effect | flags | requiredArgs | purpose |
223
224
  |------------------|------|--------|-------|--------------|---------|
224
225
  | `time_entries.count` | count | read | — | none | Count time entry records matching a JSON filter. |
225
226
  | `time_entries.customRange` | customRange | read | — | from, to | Get time entries for an explicit dateWorked range. |
226
- | `time_entries.dateWorkedWindow` | dateWorkedWindow | read | — | from, to | The unambiguous-named alias for time_entries.postedWithin (issue #66): the dateWorked window read with the posted/unposted split, counted in one pass — same wire, same result (incl. truncated). The window is ON dateWorked (entries worked within [from, to]); "postedWithin" keeps its historical name (its "posted" is the posted/unposted split over the returned rows, never a posted-date window). |
227
+ | `time_entries.dateWorkedWindow` | dateWorkedWindow | read | — | from, to | The unambiguous-named alias for time_entries.postedWithin (issue #66): the dateWorked window read with the posted/unposted split derived from the billing_items.timeEntryID join — same wire, same result (incl. truncated). The window is ON dateWorked (entries worked within [from, to]); "postedWithin" keeps its historical name (its "posted" is the posted/unposted split, never a posted-date window or a postedDate read). |
227
228
  | `time_entries.get` | get | read | — | id | Get one TimeEntry by id. |
228
229
  | `time_entries.getEntityInfo` | getEntityInfo | read | — | none | Get the TimeEntry capability metadata (canCreate/canUpdate/hasUserDefinedFields/...). |
229
230
  | `time_entries.getFieldInfo` | getFieldInfo | read | — | none | Get the TimeEntry field catalog (types, picklists, required, maxLength) — the full raw field dump (the largest read of the info trio; compact discovery: operations.fieldCatalog). |
230
- | `time_entries.getPosted` | getPosted | read | — | none | Get posted time entries (optionally for one ticket). |
231
+ | `time_entries.getPosted` | getPosted | read | — | none | Get posted time entries (an entry that a billing item references), optionally for one ticket and/or a dateWorked window — posted state derives from the billing_items.timeEntryID join, never a postedDate read or approval. |
231
232
  | `time_entries.getUdfInfo` | getUdfInfo | read | — | none | Get the TimeEntry user-defined-field catalog. |
232
- | `time_entries.getUnposted` | getUnposted | read | — | none | Get unposted time entries (optionally for one ticket). |
233
+ | `time_entries.getUnposted` | getUnposted | read | — | none | Get unposted time entries (entries no billing item references), optionally for one ticket and/or a dateWorked window — posted state derives from the billing_items.timeEntryID join, never a postedDate read or approval. |
233
234
  | `time_entries.last14` | last14 | read | — | none | Get time entries for the last 14 days (rolling, dateWorked). |
234
235
  | `time_entries.last24h` | last24h | read | — | none | Get time entries for the last 24 hours (rolling, dateWorked). |
235
236
  | `time_entries.last30` | last30 | read | — | none | Get time entries for the last 30 days (rolling, dateWorked). |
@@ -239,7 +240,7 @@ Tool input: {"operation":{"type":"string","enum":["transferOwnership","update"]}
239
240
  | `time_entries.lastFullMonth` | lastFullMonth | read | — | none | Get time entries for the last complete calendar month. |
240
241
  | `time_entries.lastFullQuarter` | lastFullQuarter | read | — | none | Get time entries for the last complete calendar quarter. |
241
242
  | `time_entries.lastFullWeek` | lastFullWeek | read | — | none | Get time entries for the last complete calendar week. |
242
- | `time_entries.postedWithin` | postedWithin | read | — | from, to | Get time entries WORKED within an inclusive dateWorked window with the posted/unposted split, counted in one pass. The window is ON the dateWorked field — the name's "posted" is the posted/unposted SPLIT counted over the returned rows (the server-side postedDate exist|notExist predicate), never a posted-date window: no postedDate window op exists (the vendored TimeEntryModel declares no postedDate filter, and ww29-class zones 500 on postedDate filters ONLY WHEN the `posted` narrowing is set — omit `posted` on such zones — W-D3). The counts cover the returned rows only; truncated flags the capped window. time_entries.dateWorkedWindow is the unambiguous name. |
243
+ | `time_entries.postedWithin` | postedWithin | read | — | from, to | Get time entries WORKED within an inclusive dateWorked window with the posted/unposted split. The window is ON the dateWorked field — the name's "posted" is the posted/unposted SPLIT derived from the billing_items.timeEntryID join (an entry a billing item references is posted, approval is not consulted), never a posted-date window or a postedDate read. The counts cover the returned rows only; truncated flags a capped read. time_entries.dateWorkedWindow is the unambiguous name. |
243
244
  | `time_entries.query` | query | read | — | none | Query TimeEntry records with a JSON filter. Bounded: server-side pagination — up to limit records (default 500, max 2500) in one call; 1000-page guard; hasMore flags a truncation. |
244
245
  | `time_entries.recentTickets` | recentTickets | read | — | from, to, opts | List the distinct tickets a resource logged time against in an inclusive dateWorked window, with entry count, total hours, last worked date and the ticket's number, title, status, assignee and company. |
245
246
  | `time_entries.resolve` | resolve | read | — | identifier | Resolve a time entry from a numeric id (the direct fetch) only — this resource has no searchable text field, so a non-numeric identifier throws a config error. |
@@ -275,19 +276,19 @@ only via a recorded change to `n8n-mcp.curation.json`.
275
276
 
276
277
  ## Stats
277
278
 
278
- - registryRecords: 1279
279
+ - registryRecords: 1280
279
280
  - curatedEntities: 7
280
281
  - includedEntities: 7
281
282
  - readTools: 7
282
283
  - writeTools: 7
283
284
  - totalTools: 14
284
- - readOperations: 88
285
+ - readOperations: 89
285
286
  - writeOperations: 22
286
287
  - approvalGatedWrites: 2
287
288
  - destructiveExcluded: 4
288
289
 
289
290
  ## Meta
290
291
 
291
- - generatedAt: 2026-10-26T22:34:43.655Z
292
- - capabilitiesHash: 68a3fbb3ceb8cf046bd966f47b7526050baa536df8199ddb21828582fcd69ff1
292
+ - generatedAt: 2026-10-27T11:39:16.557Z
293
+ - capabilitiesHash: ba97b65fc4424ba19d524a07a92892920d809ab6aa37fc7c79d849725e73861b
293
294
  - curationHash: e19117ce73e38d9aded3be7e7f077a4e185d2e10294622b76c17d67f63cf9e3b
package/README.md CHANGED
@@ -349,7 +349,7 @@ The SDK ships the mechanical substrate, not a server (no MCP imports, no zod):
349
349
  whose MCP annotation is `readOnlyHint !== true` — today exactly **267
350
350
  tools**: the 265 dedicated write/delete tools (209 `effect=write` + 56
351
351
  `effect=destructive`, every one of them has a dedicated tool) +
352
- `autotask_write` + `autotask_delete`. The remaining **633 tools** (the 620 dedicated read tools + the 10 core read tools + `autotask_read` +
352
+ `autotask_write` + `autotask_delete`. The remaining **634 tools** (the 621 dedicated read tools + the 10 core read tools + `autotask_read` +
353
353
  `autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true`
354
354
  and are safe ungated: every op they can route is `effect=read` (zero
355
355
  mutating requests — pinned by the test inventory). Refined class for the
@@ -398,10 +398,11 @@ The SDK ships the mechanical substrate, not a server (no MCP imports, no zod):
398
398
  blind count/query surfaces (companies, unfiltered billing_codes, resources
399
399
  — `get(id)` is the authority), the isActive-is-a-number quirk, the
400
400
  time-entry `internalBillingCodeID` wire key, and the W-D3 posted-state
401
- filter wall on this zone's `time_entries` (the live TimeEntry entity has
402
- NO posted-state filter field — `time_entries.postedWithin` is the working
403
- window read; `getPosted` / `getUnposted` 500 there is the typed known
404
- wall, the pinned pointer note rides the error). Since the served-metadata
401
+ gap on this zone's `time_entries` (the live TimeEntry entity has NO
402
+ `postedDate`; `getPosted` / `getUnposted` / `postedWithin` derive posted
403
+ state from the `billing_items.timeEntryID` join (any billing item referencing the entry),
404
+ and `getPosted` / `getUnposted` return `{ items, hasMore, window,
405
+ approvedCount, unapprovedCount }`). Since the served-metadata
405
406
  maturity pass (S12-C): the 6 pinned blind surfaces attach a TYPED
406
407
  `blindSurface` sentinel to 0/empty count/query results at read time
407
408
  (§11.7 — the result-time enforcement of the §11.2 rule), the top-create
@@ -582,7 +583,8 @@ suites are the release gate, the live scripts are the ground-truth check.
582
583
  + `npm run verify`, the Node-24 floor, the per-tenant 429 cooldown, delete
583
584
  boundaries + `operations.recovery`, the blind-surface inventory + bounded
584
585
  probe, the zone-fact create-guidance catalog, the posted/unposted rework
585
- (spec-correct `postedDate` wire + the live-verified W-D3 typed wall +
586
+ (first the spec `postedDate` wire, replaced in 0.32.0 by the
587
+ billing-item join because ww29 has no `postedDate`; W-D3 +
586
588
  `postedWithin`), the create-path marker recovery, the typed `emptyReason`,
587
589
  and the served-error `details` pass-through) — live-verified on the ww29
588
590
  sandbox with real reads AND writes (marker-tagged, zero residue;