node-autotask 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/MCP_TOOL_CATALOG.json +921 -30
- package/MCP_TOOL_MANIFEST.md +69 -16
- package/N8N_MCP_MANIFEST.json +39 -8
- package/N8N_MCP_MANIFEST.md +9 -8
- package/README.md +11 -9
- package/capabilities.json +13316 -7733
- package/capabilities.schema.json +125 -0
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-SSUECUVN.js → chunk-42ZUISDH.js} +947 -128
- package/dist/{chunk-WM6E5O55.cjs → chunk-5CC7LGNM.cjs} +13315 -7732
- package/dist/{chunk-SG4LIVMG.cjs → chunk-5GGCSRT4.cjs} +848 -439
- package/dist/{chunk-QXWIQ67O.js → chunk-F7FVOXPQ.js} +13315 -7732
- package/dist/{chunk-7DOSJS6S.js → chunk-GURC52J7.js} +23 -2
- package/dist/{chunk-5OGJTS2E.cjs → chunk-UHTZLUDE.cjs} +157 -136
- package/dist/{chunk-VVQ6NJYZ.js → chunk-ZGTFVZL5.js} +433 -24
- package/dist/{chunk-HQGE4WAV.cjs → chunk-ZJ4ONAQF.cjs} +1054 -235
- package/dist/{index-CKYr0unq.d.cts → index-BIWy9m-a.d.ts} +379 -49
- package/dist/{index-C8QH3BkP.d.ts → index-D4XYtn4s.d.cts} +379 -49
- package/dist/{index-1zBdJIEy.d.cts → index-DQ9NgraW.d.cts} +281 -1
- package/dist/{index-1zBdJIEy.d.ts → index-DQ9NgraW.d.ts} +281 -1
- package/dist/index.cjs +44 -24
- package/dist/index.d.cts +11 -5
- package/dist/index.d.ts +11 -5
- package/dist/index.js +26 -6
- package/dist/operations/index.cjs +147 -120
- package/dist/operations/index.d.cts +136 -7
- package/dist/operations/index.d.ts +136 -7
- package/dist/operations/index.js +33 -6
- package/dist/pool.cjs +42 -9
- package/dist/pool.d.cts +14 -3
- package/dist/pool.d.ts +14 -3
- package/dist/pool.js +38 -5
- package/dist/resources/index.cjs +19 -5
- package/dist/resources/index.d.cts +2 -2
- package/dist/resources/index.d.ts +2 -2
- package/dist/resources/index.js +18 -4
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.d.cts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/docs/API.md +207 -9
- package/examples/mcp/schemas.ts +16 -7
- package/package.json +1 -1
- /package/dist/{chunk-S643FDOR.cjs → chunk-IXF7AKOJ.cjs} +0 -0
- /package/dist/{chunk-2TWKEB2L.js → chunk-RMG55JTF.js} +0 -0
- /package/dist/{chunk-W3K5BEVY.cjs → chunk-SDYRQFGL.cjs} +0 -0
- /package/dist/{chunk-EBCHCNPK.js → chunk-WLTLB2ND.js} +0 -0
package/docs/API.md
CHANGED
|
@@ -930,6 +930,29 @@ rejects the batch once in-flight items settle; `signal` stops new items starting
|
|
|
930
930
|
`node-autotask/resources` (resource classes) and `node-autotask/types` (entity
|
|
931
931
|
types, type-only) alongside `./capabilities` and `./operations`.
|
|
932
932
|
|
|
933
|
+
### Pooled clients and the shared read cache (`./pool` subpath, issue #219 N6)
|
|
934
|
+
|
|
935
|
+
`createClientPool({ maxSize?, idleTtlMs?, factory?, sharedReadCache? })` keeps
|
|
936
|
+
one `AutotaskClient` per credential tuple (`{ get, delete, clear, size }`) so a
|
|
937
|
+
multi-tenant gateway reuses each tenant's limiter state. Each pooled client has
|
|
938
|
+
its own read cache, so a write through one client never cleared what another
|
|
939
|
+
client of the SAME tenant would serve — a post-write read-back routed to a
|
|
940
|
+
re-created client (a `delete(config)` on a 401, an LRU eviction, an idle
|
|
941
|
+
expiry) could return the pre-write snapshot for up to the resource TTL
|
|
942
|
+
(contacts: 5 min).
|
|
943
|
+
|
|
944
|
+
**Default (shared):** the pool OWNS one `ReadCache` per credential tuple and
|
|
945
|
+
hands it to every client it builds for that tuple (via the `cache.store`
|
|
946
|
+
config seam), so any write through any pooled client of that tenant voids the
|
|
947
|
+
read cache for all of them and a read-back is never served the pre-write
|
|
948
|
+
snapshot. The shared cache outlives a client entry's `delete`/eviction (its
|
|
949
|
+
entries still expire on their own TTL), is pruned at `maxSize` and dropped by
|
|
950
|
+
`clear()`. `sharedReadCache: false` restores per-client caches (then route a
|
|
951
|
+
post-write read-back through the writing client). `cache: false` opts the tuple
|
|
952
|
+
out entirely. `cache.store` is otherwise a plain advanced seam: a supplied
|
|
953
|
+
`ReadCache` instance is used as-is, and `ttlMs` / `perResourceTtlMs` beside it
|
|
954
|
+
refuse (the instance owns its TTLs).
|
|
955
|
+
|
|
933
956
|
## 7. Operations surface (`./operations` subpath)
|
|
934
957
|
|
|
935
958
|
Cross-resource helpers live in `src/operations/` (placement locked —
|
|
@@ -1058,13 +1081,13 @@ be cross-mode-refused.
|
|
|
1058
1081
|
and the `confirm === <op key>` gate (all 54 deletes; the 3 approval-gated
|
|
1059
1082
|
writes — inside `autotask_write` too) apply exactly as in `invoke`.
|
|
1060
1083
|
Pinned by the full-surface suite (`test/operations/invoke/modes.test.ts`:
|
|
1061
|
-
all
|
|
1084
|
+
all 1275 ops × 3 modes, zero wire).
|
|
1062
1085
|
|
|
1063
1086
|
**Harness approval policy (recommended).** Require approval on every tool
|
|
1064
|
-
whose MCP annotation is `readOnlyHint !== true` — today exactly **
|
|
1065
|
-
tools**: the
|
|
1087
|
+
whose MCP annotation is `readOnlyHint !== true` — today exactly **267
|
|
1088
|
+
tools**: the 265 dedicated write/delete tools (209 `effect=write` + 56
|
|
1066
1089
|
`effect=destructive`, every one of them has a dedicated tool) +
|
|
1067
|
-
`autotask_write` + `autotask_delete`. The remaining **
|
|
1090
|
+
`autotask_write` + `autotask_delete`. The remaining **629 tools** (the 616
|
|
1068
1091
|
dedicated read tools + the 10 core read tools + `autotask_read` +
|
|
1069
1092
|
`autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true`
|
|
1070
1093
|
and are safe ungated: every op they can route is `effect=read` (zero
|
|
@@ -1077,7 +1100,7 @@ additionally require `confirm === <operation key>` even inside
|
|
|
1077
1100
|
`resources.transferOwnership`) require the same confirm inside
|
|
1078
1101
|
`autotask_write`. Migration: `autotask_invoke` is REPLACED by
|
|
1079
1102
|
`autotask_read`/`autotask_write`/`autotask_delete` — update harness config
|
|
1080
|
-
(approve the
|
|
1103
|
+
(approve the 267 above) and remove any `autotask_invoke` entry. The rule is
|
|
1081
1104
|
derivable from the registry `effect` column (and the mode tools' fixed
|
|
1082
1105
|
annotations) — it never needs a hand-maintained name list.
|
|
1083
1106
|
|
|
@@ -1418,7 +1441,39 @@ notes cannot be deleted, so they never use create-then-delete.
|
|
|
1418
1441
|
calendar dates the window covers in that zone and the bounds are whole UTC days of those dates. (Before 0.24.0 a `tz`
|
|
1419
1442
|
behind UTC, e.g. `America/Los_Angeles`, selected the NEXT UTC day; live-verified 2026-10-23.)
|
|
1420
1443
|
|
|
1421
|
-
|
|
1444
|
+
### 7.12 `timeEntries.approve` — time-entry approval levels (2026-10-26)
|
|
1445
|
+
|
|
1446
|
+
`timeEntries.approve(id, opts?)` records one approval level for a time entry.
|
|
1447
|
+
The vendor keeps approvals in a separate entity (`BillingItemApprovalLevels`)
|
|
1448
|
+
and, by its own documentation, does not write the approval fields on the time
|
|
1449
|
+
entry when that record is created — so the helper is a composite:
|
|
1450
|
+
|
|
1451
|
+
1. reads the time entry (also the whole dry run: one GET, zero writes),
|
|
1452
|
+
2. `POST /V1.0/BillingItemApprovalLevels` with
|
|
1453
|
+
`{ timeEntryID, approvalResourceID, approvalLevel, approvalDateTime }`,
|
|
1454
|
+
3. updates the time entry with the vendor's own workaround —
|
|
1455
|
+
`billingApprovalDateTime` + `billingApprovalResourceID`, one
|
|
1456
|
+
collection-merge `PUT /V1.0/TimeEntries`,
|
|
1457
|
+
4. reads the entry back and compares the two fields.
|
|
1458
|
+
|
|
1459
|
+
Defaults: `approvalResourceID` = the entry's own `resourceID`, `approvalLevel` =
|
|
1460
|
+
1, `approvalDateTime` = now. An entry with no `resourceID` and no caller pin is
|
|
1461
|
+
refused before any write.
|
|
1462
|
+
|
|
1463
|
+
The result is `status: 'approved'` when the read-back matches, and
|
|
1464
|
+
`status: 'approval_fields_not_persisted'` (`persisted: false`) when the level
|
|
1465
|
+
record was created but the follow-up fields did not stick. The level id is
|
|
1466
|
+
returned either way: the vendor defines no DELETE for
|
|
1467
|
+
`BillingItemApprovalLevels`, so the record cannot be rolled back.
|
|
1468
|
+
`dryRun: true` returns the pinned envelope for the POST plus the same warning,
|
|
1469
|
+
with the single GET as the only wire call.
|
|
1470
|
+
|
|
1471
|
+
Posting is not part of the operation (`/V1.0/BillingItems` exposes PUT/PATCH
|
|
1472
|
+
only), and the typed TimeEntry model omits the live-only `postedDate` /
|
|
1473
|
+
`postedOnTime` fields, so "approved and posted" is only detectable as a vendor
|
|
1474
|
+
error from the follow-up update.
|
|
1475
|
+
|
|
1476
|
+
## 8. Resource surface (135 resources)
|
|
1422
1477
|
|
|
1423
1478
|
<!-- BEGIN GENERATED:api-per-entity -->
|
|
1424
1479
|
Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page guarded), `queryBounded` (server-side pagination — one call walks the cursor until `limit` TOTAL records, default 500 / max 2500, with a `hasMore` flag; the MCP `query` surface, routed by `invoke`), `queryAll` (collect → `T[]` — the unbounded raw walk), `count`, `get` (id → record; `item: null` → `NotFoundError`), `create` / `update` / `delete` (where the vendor defines them — mutations take a `MutationOpts` bag: `{ dryRun, lastModifiedDate?, maxRetries? }` (issue #105: `maxRetries` overrides the configured retry budget for every request the mutation issues; `0` = exactly one send per request)), plus the helper floor where the plan declares it: `resolve` (all 121), `search` (40 — bounded contains on the resource text field), `getContext` (5 workflow resources), `findBy<Field>` (3), `createIfNotExists` (14). Entity metadata on every resource: `getEntityInfo`, `getFieldInfo`, `getUdfInfo` (UDF-capable 15). Parent-scoped children take `(parentId, id)` on the CRUD methods and require `parentId` on `createIfNotExists` (the scoped create POSTs the scoped collection; the dedup query stays global); `query`/`count`/`resolve`/`search` stay global (narrow with a `parentField` filter). Scoped `update` PATCHes the collection (the vendor keys the record by the id in the body).
|
|
@@ -1427,6 +1482,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1427
1482
|
|--------------|-----------|------|------|--------|----------------------|-------------|---------|
|
|
1428
1483
|
| `appointments` | Appointment | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_appointment_get |
|
|
1429
1484
|
| `billing_codes` | BillingCode | entity | extended | no | query, count, get, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_billingCode_get |
|
|
1485
|
+
| `billing_item_approval_levels` | BillingItemApprovalLevel | entity | extended | no | query, count, get, create | — | autotask_billingItemApprovalLevel_get |
|
|
1430
1486
|
| `billing_items` | BillingItems | entity | extended | no | query, count, get, update, resolve | customRange, getEntityInfo, getFieldInfo, getUdfInfo, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, today, yesterday | autotask_billingItems_get |
|
|
1431
1487
|
| `change_request_links` | ChangeRequestLink | entity | extended | no | query, count, get, create, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_changeRequestLinks_get |
|
|
1432
1488
|
| `checklist_libraries` | ChecklistLibrary | entity | extended | no | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_checklistLibrary_get |
|
|
@@ -1482,11 +1538,15 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1482
1538
|
| `expense_reports` | ExpenseReport | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_expenseReport_get |
|
|
1483
1539
|
| `holiday_sets` | HolidaySet | entity | advanced | no | query, count, get, create, update, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_holidaySet_get |
|
|
1484
1540
|
| `holidays` | Holiday | entity | advanced | yes (holiday_sets) | query, count, get, create, update, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_holiday_get |
|
|
1541
|
+
| `invoice_templates` | InvoiceTemplate | entity | advanced | no | query, count, get | getEntityInfo, getFieldInfo, getUdfInfo | autotask_invoiceTemplate_get |
|
|
1485
1542
|
| `invoices` | Invoice | entity | extended | no | query, count, get, update, resolve | customRange, getEntityInfo, getFieldInfo, getUdfInfo, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, markupHtml, markupXml, pdf, today, yesterday | autotask_invoice_get |
|
|
1486
1543
|
| `notification_history` | NotificationHistory | entity | advanced | no | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_notificationHistory_get |
|
|
1487
1544
|
| `opportunities` | Opportunity | entity | extended | no | query, count, get, create, update, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunity_get |
|
|
1488
1545
|
| `opportunity_attachments` | OpportunityAttachment | entity | extended | yes (opportunities) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunityAttachment_get |
|
|
1489
1546
|
| `opportunity_categories` | OpportunityCategory | entity | extended | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunityCategories_get |
|
|
1547
|
+
| `organizational_level1s` | OrganizationalLevel1 | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevel1_get |
|
|
1548
|
+
| `organizational_level2s` | OrganizationalLevel2 | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevel2_get |
|
|
1549
|
+
| `organizational_level_associations` | OrganizationalLevelAssociation | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevelAssociation_get |
|
|
1490
1550
|
| `product_vendors` | ProductVendors | entity | extended | yes (products) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_productVendor_get |
|
|
1491
1551
|
| `products` | Product | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_product_get |
|
|
1492
1552
|
| `project_charges` | ProjectCharge | entity | extended | yes (projects) | query, count, get, create, update, delete, resolve, search | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_projectCharge_get |
|
|
@@ -1510,6 +1570,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1510
1570
|
| `resource_time_off_balances` | ResourceTimeOffBalance | entity | extended | yes (resources) | get, resolve | getByResource, getByYear, getEntityInfo, getFieldInfo, getUdfInfo | autotask_resourceTimeOffBalance_get |
|
|
1511
1571
|
| `resources` | Resource | entity | core | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo, transferOwnership, whoAmI | autotask_resource_get |
|
|
1512
1572
|
| `roles` | Role | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_role_get |
|
|
1573
|
+
| `sales_orders` | SalesOrder | entity | extended | no | query, count, get, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_salesOrder_get |
|
|
1513
1574
|
| `service_call_task_resources` | ServiceCallTaskResource | entity | extended | yes (service_call_tasks) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTaskResource_get |
|
|
1514
1575
|
| `service_call_tasks` | ServiceCallTask | entity | extended | yes (service_calls) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTask_get |
|
|
1515
1576
|
| `service_call_ticket_resources` | ServiceCallTicketResource | entity | extended | yes (service_call_tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTicketResource_get |
|
|
@@ -1526,6 +1587,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1526
1587
|
| `tag_groups` | TagGroup | entity | advanced | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_tagGroup_get |
|
|
1527
1588
|
| `tags` | Tag | entity | advanced | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_tag_get |
|
|
1528
1589
|
| `task_secondary_resources` | TaskSecondaryResource | entity | extended | yes (tasks) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_taskSecondaryResources_get |
|
|
1590
|
+
| `tax_regions` | TaxRegion | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_taxRegion_get |
|
|
1529
1591
|
| `ticket_additional_configuration_items` | TicketAdditionalConfigurationItem | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAdditionalConfigurationItem_get |
|
|
1530
1592
|
| `ticket_additional_contacts` | TicketAdditionalContact | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAdditionalContact_get |
|
|
1531
1593
|
| `ticket_attachments` | TicketAttachment | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAttachment_get |
|
|
@@ -1542,7 +1604,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1542
1604
|
| `ticket_secondary_resources` | TicketSecondaryResource | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketSecondaryResources_get |
|
|
1543
1605
|
| `ticket_webhooks` | TicketWebhook | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketWebhook_get |
|
|
1544
1606
|
| `tickets` | Ticket | entity | core | no | query, count, get, create, update, resolve, search, getContext | changeStatus, findByTicketNumber, fullContext, getEntityInfo, getFieldInfo, getUdfInfo, openWithNoteAndAssign, slaHealthCheck, summary, timeline | autotask_ticket_get |
|
|
1545
|
-
| `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve | createForResource, createIfNotExists, customRange, dateWorkedWindow, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, postedWithin, today, yesterday | autotask_timeEntry_get |
|
|
1607
|
+
| `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve | approve, createForResource, createIfNotExists, customRange, dateWorkedWindow, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, postedWithin, today, yesterday | autotask_timeEntry_get |
|
|
1546
1608
|
| `time_entry_attachments` | TimeEntryAttachment | entity | extended | yes (time_entries) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_timeEntryAttachment_get |
|
|
1547
1609
|
| `time_off_requests` | TimeOffRequest | entity | extended | no | query, count, get, resolve | approve, getEntityInfo, getFieldInfo, getUdfInfo, reject | autotask_timeOffRequest_get |
|
|
1548
1610
|
| `inventory_products` | InventoryProduct | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo, stockedItemsAdd, stockedItemsRemove, stockedItemsTransfer | autotask_inventoryProduct_get |
|
|
@@ -1554,7 +1616,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1554
1616
|
| `threshold_information` | — | integration | core | no | get | — | autotask_threshold_information |
|
|
1555
1617
|
| `zone_information` | — | integration | advanced | no | get | — | autotask_zone_information |
|
|
1556
1618
|
|
|
1557
|
-
|
|
1619
|
+
129 resources (122 entity + 7 integration). Rows are generated from the registry — do not hand-type.
|
|
1558
1620
|
<!-- END GENERATED:api-per-entity -->
|
|
1559
1621
|
|
|
1560
1622
|
**`companies.search` include_family (issue #59).** `companies.search` is
|
|
@@ -1831,6 +1893,28 @@ resource paths, of which **40** expose a create op (the stampable set; the
|
|
|
1831
1893
|
(resource, role) pairs exist in the tenant (postedWithin rows: resourceID
|
|
1832
1894
|
31131373 / 29869529 with roleID 29682834). The typed W-A3 pointer note
|
|
1833
1895
|
carries this.
|
|
1896
|
+
- **Derived fields (issue #219 N8, live ww29 2026-10-26 — twelve time entries
|
|
1897
|
+
logged on ticket 530637)**: `hoursWorked` is recomputed from
|
|
1898
|
+
`startDateTime`/`endDateTime` (a create sending `1.43` for a 14:00-15:26
|
|
1899
|
+
block stored `1.5`; omitting it stored the same `1.5`; other entries stored
|
|
1900
|
+
the exact elapsed time — 40 min → 0.6667, 110 min → 1.8333, 15 min →
|
|
1901
|
+
0.25; the rounding rule is **UNVERIFIED**) and a later PATCH returns HTTP
|
|
1902
|
+
success without persisting a correction (`FIELD_NOT_PERSISTED`) — the only
|
|
1903
|
+
remedy is delete + recreate. `timeEntryType`, `contractID` and
|
|
1904
|
+
`internalBillingCodeID` are inherited (from the ticket; the internal billing
|
|
1905
|
+
code is mirrored from `billingCodeID`); `timeEntryType` is flagged
|
|
1906
|
+
`readOnly`, which reads as "never send" but means "derived from the parent".
|
|
1907
|
+
`billingApprovalLevelMostRecent` is derived by a DIFFERENT entity: the REST
|
|
1908
|
+
approval write is a **create on `BillingItemApprovalLevels`** (create-only;
|
|
1909
|
+
`billing_item_approval_levels` is a plan resource since 0.27.0), while
|
|
1910
|
+
`billingApprovalResourceID` and `billingApprovalDateTime` are a documented
|
|
1911
|
+
follow-up UPDATE of the entry, not the approval itself — a caller told to
|
|
1912
|
+
"approve" must create the approval level row, not write two fields. The
|
|
1913
|
+
field is also not a usable signal: it stayed `0` for those twelve entries
|
|
1914
|
+
and for all 392,182 posted billing items in the tenant. The declaration is
|
|
1915
|
+
plan-pinned (`resources.time_entries.derivedFields`, tokens only — §11.14)
|
|
1916
|
+
and served on `operations.fieldCatalog` rows and the `time_entries`
|
|
1917
|
+
`createGuidance` row.
|
|
1834
1918
|
|
|
1835
1919
|
### 11.5 Company-create side effect
|
|
1836
1920
|
|
|
@@ -2095,7 +2179,7 @@ calls — generated plan data, drift-guarded by `capabilities:check` clause
|
|
|
2095
2179
|
14), so an agent can read "what is blind on this zone" without burning
|
|
2096
2180
|
budget or guessing:
|
|
2097
2181
|
|
|
2098
|
-
- **`operations.blindSurfaces`** — the
|
|
2182
|
+
- **`operations.blindSurfaces`** — the 124-surface inventory: per surface the
|
|
2099
2183
|
kind (`unowned-blind` / `unfiltered-blind` / `unknown` — the 46
|
|
2100
2184
|
data-with-rows surfaces ride `unknown` by design: the sweep's limit-2 read
|
|
2101
2185
|
proves data, not the absence of it), the recorded `unfilteredRows` (0-2
|
|
@@ -2133,6 +2217,57 @@ W-A7 drift — the 200 that is not the specced envelope). An agent that reads
|
|
|
2133
2217
|
the row before its create attempt converts a multi-iteration vendor ladder
|
|
2134
2218
|
into one predicted-failure-free attempt.
|
|
2135
2219
|
|
|
2220
|
+
**Which required set wins (issue #219 N4).** Three sources answer "what does
|
|
2221
|
+
this create really require", at different coverage: the live-proven prefix
|
|
2222
|
+
(92 rows — the fields a live create actually demanded, in vendor-demand
|
|
2223
|
+
order), the hand-authored zone set `createRequiredSet` (6 resources), and the
|
|
2224
|
+
vendor field catalog's `isRequired` (the spec opinion — wrong for 68 of the
|
|
2225
|
+
75 resources that carry a proven prefix). The precedence is FIXED:
|
|
2226
|
+
**live-proven > zone > spec**, and the source is named on the result.
|
|
2227
|
+
`effectiveRequiredFields(record, zoneFacts)` in `node-autotask/operations` is
|
|
2228
|
+
the only place in the SDK that compares the three; every
|
|
2229
|
+
`operations.createGuidance` row carries its result as
|
|
2230
|
+
`requiredFields: { fields, from: 'live-proven' | 'zone' | 'spec' }`
|
|
2231
|
+
(`contacts.create` → the four live-proven fields, not the empty spec list).
|
|
2232
|
+
A value-conditioned prefix (e.g. `contracts.create`'s type-1
|
|
2233
|
+
`estimatedRevenue`/`estimatedHours`/`estimatedCost`) is returned whole with
|
|
2234
|
+
`from: 'live-proven'` — the condition is typed on the row (see the next
|
|
2235
|
+
paragraph), so read it before treating the list as unconditional.
|
|
2236
|
+
|
|
2237
|
+
**Structured create requirements (issue #219 N3).** The blocker channel is a
|
|
2238
|
+
CLASS of wall, not a statement of what the vendor wants; and the campaign
|
|
2239
|
+
receipt that DID say what it wanted is 300-char truncated on disk
|
|
2240
|
+
(`ticket_charges.create` preserves only `"A productID or a billingCodeID must b"`).
|
|
2241
|
+
So the requirement is **declared** where the fact is known, and the receipt
|
|
2242
|
+
stays evidence — never reconstructed:
|
|
2243
|
+
|
|
2244
|
+
- `blocker.requires?: { anyOf?, allOf?, noneOf? }` — the vendor field names a
|
|
2245
|
+
caller must supply (or must not) for the create to be accepted. Optional and
|
|
2246
|
+
additive: a row without it keeps its exact shape.
|
|
2247
|
+
`ticket_charges.create` and `project_charges.create` serve
|
|
2248
|
+
`requires: { anyOf: ['productID', 'billingCodeID'] }`.
|
|
2249
|
+
- `createGuidance.requiresByValue?: [{ field, value, requires, values? }]` —
|
|
2250
|
+
requirements conditioned on a picklist value, so a caller sees the condition
|
|
2251
|
+
and can satisfy it before the attempt. `contracts.create` declares the
|
|
2252
|
+
recorded `contractType` gates: type 1 → `estimatedRevenue` +
|
|
2253
|
+
`estimatedHours` + `estimatedCost`; types 3, 4, 6, 8 → `estimatedRevenue`;
|
|
2254
|
+
type 7 → `contractPeriodType` (+ `values: { contractPeriodType: 2 }`) and
|
|
2255
|
+
`setupFee`. (Whether type 7 also needs `estimatedRevenue` is **UNVERIFIED**:
|
|
2256
|
+
the recorded message names `estimatedRevenue` for types 1/3/4/6/8 only, and
|
|
2257
|
+
this block reproduces that clause exactly rather than extending it.)
|
|
2258
|
+
- `createGuidance.rejectedValues?: [{ field, value }]` — picklist values the
|
|
2259
|
+
vendor refuses (`contractType: 5`).
|
|
2260
|
+
|
|
2261
|
+
The prose those fields used to live in is gone: the `contracts.create` message
|
|
2262
|
+
now POINTS at `requiresByValue` instead of restating the gates, so no consumer
|
|
2263
|
+
has to parse English. `check-capabilities` clause 20 holds the shape closed,
|
|
2264
|
+
checks every named field against the vendored model of the resource's
|
|
2265
|
+
`apiEntity`, and fails when a row's `vendorEvidence` states a one-of
|
|
2266
|
+
requirement (`"… or … must …"`) while the row declares no `requires` — the
|
|
2267
|
+
clause that stops this fact from being lost a second time. The create op's
|
|
2268
|
+
registry record carries the same typed block as `metadata.createRequirements`
|
|
2269
|
+
(the `autotask_describe` channel).
|
|
2270
|
+
|
|
2136
2271
|
### 11.13 The posted/unposted wall (W-D3) + `postedWithin`, and drifted-create marker recovery (issues #10/#13)
|
|
2137
2272
|
|
|
2138
2273
|
- **W-D3 (live, ww29, 2026-10-04, re-verified)** — this zone's TimeEntry
|
|
@@ -2251,6 +2386,64 @@ the field catalog; the seven integration singletons are refused typed —
|
|
|
2251
2386
|
a known key without a catalog → `CONFIG_ERROR`, an unknown key → typed
|
|
2252
2387
|
`RESOURCE_UNKNOWN` carrying the count + the `autotask_catalog` pointer).
|
|
2253
2388
|
|
|
2389
|
+
**Reference targets (issue #219 N1).** Every projected field row carries
|
|
2390
|
+
`targetResource: string | null` and `targetEnumerable: boolean`: the plan
|
|
2391
|
+
resource key the vendor `referenceEntityType` names, and whether that
|
|
2392
|
+
resource has a read (`query`/`get`) path, i.e. whether a caller can
|
|
2393
|
+
enumerate valid ids with it. The join is DERIVED from the generated
|
|
2394
|
+
registry — `resourceForEntityType(entityType)` in
|
|
2395
|
+
`node-autotask/operations` reads each resource's plan `apiEntity` (plus
|
|
2396
|
+
the singular entity name where the plan's `apiEntity` is the plural path
|
|
2397
|
+
segment) and returns `null` when the plan carries no such resource, so a
|
|
2398
|
+
consumer never hand-maps `"CompanyLocation"` -> `company_locations`. A
|
|
2399
|
+
row for a target the plan cannot enumerate reads `null` + `false`; the
|
|
2400
|
+
row carries no remedy, label or exposure claim (`plan.referenceTargets`
|
|
2401
|
+
is the only home for those). The same helper serves reference UDF rows —
|
|
2402
|
+
there is no second entity-type table.
|
|
2403
|
+
|
|
2404
|
+
**User-defined fields (issue #219 N7).** `udfCapable` entities carry a
|
|
2405
|
+
parallel field system, and the served row states it rather than leaving it
|
|
2406
|
+
inferred. With the `udf` include, the row's `udf` list holds the UDF
|
|
2407
|
+
DEFINITIONS the vendor's `entityInformation/userDefinedFields` channel
|
|
2408
|
+
returns — `name`, `label`, `type`, `isRequired`, `isReadOnly`, `isPickList`,
|
|
2409
|
+
`isReference`, `referenceEntityType`, `picklistValues` (the values ride the
|
|
2410
|
+
`labels: true` opt, zero extra wire), `defaultValue`. A UDF definition never
|
|
2411
|
+
appears in the plan artifact (it is tenant-specific). The row also carries
|
|
2412
|
+
`udfFilter: 'udf-marker'` and `udfWriteShape: 'merge'` exactly when it is
|
|
2413
|
+
`udfCapable`: a filter on a UDF NAME is accepted by the vendor only with the
|
|
2414
|
+
`udf: true` marker, so a UDF field row serves `filterable: 'udf-marker'` and
|
|
2415
|
+
omits the vendor's `isQueryable` (true for UDFs, contradicted by the plain
|
|
2416
|
+
filter refusal), and a sent `userDefinedFields` list is MERGED by the vendor
|
|
2417
|
+
(a subset is correct). Object/array field rows declare
|
|
2418
|
+
`writeShape: 'merge' | 'replace-all'` (a scalar row carries none);
|
|
2419
|
+
`userDefinedFields` is `merge` at the row level. The row type
|
|
2420
|
+
(`FieldCatalogFieldRow`) is hand-written in `src/operations/fieldCatalog.ts`,
|
|
2421
|
+
not generated from the plan — the UDF vocabulary is a LIVE read.
|
|
2422
|
+
|
|
2423
|
+
**Derived fields (issue #219 N8).** Some fields are OUTPUTS: the vendor
|
|
2424
|
+
recomputes them or inherits them, so a submitted value is not authoritative.
|
|
2425
|
+
The plan pins the declaration on the resource
|
|
2426
|
+
(`resources.<key>.derivedFields: [{ field, rule, writableRoute? }]`) and the
|
|
2427
|
+
catalog serves it twice: the resource row carries the whole `derivedFields`
|
|
2428
|
+
list (present exactly when the plan declares one, never filtered by the
|
|
2429
|
+
`fields` opt) and each matching projected field row carries
|
|
2430
|
+
`derived: { rule, writableRoute? }`. `rule` is a closed token, never a
|
|
2431
|
+
sentence: `elapsed(<startField>,<endField>)` (the vendor recomputes the value
|
|
2432
|
+
from those two timestamps), `parent` (inherited from the parent record),
|
|
2433
|
+
`mirror(<field>)` (the vendor copies that field) and `external` (written by
|
|
2434
|
+
another entity's op — `writableRoute` names the plan write op). The tokens are
|
|
2435
|
+
validated by `check-capabilities` clause 21, which also pins the
|
|
2436
|
+
`time_entries` set (see §11.4/§11.12): `hoursWorked` is
|
|
2437
|
+
`elapsed(startDateTime,endDateTime)`, `timeEntryType` and `contractID` are
|
|
2438
|
+
`parent`, `internalBillingCodeID` is `mirror(billingCodeID)`, and
|
|
2439
|
+
`billingApprovalLevelMostRecent` is `external` with
|
|
2440
|
+
`writableRoute: billing_item_approval_levels.create`. **`derived` is not
|
|
2441
|
+
`readOnly`:** `isReadOnly` (the vendor flag) means never write the field at
|
|
2442
|
+
all; `derived` means the vendor overwrites or inherits it — a field can be
|
|
2443
|
+
derived and not read-only (`hoursWorked` accepts a sent value and discards
|
|
2444
|
+
it). The registry stamps the same list as `metadata.derivedFields` on every
|
|
2445
|
+
record of the resource, so `autotask_describe` and the catalog cannot drift.
|
|
2446
|
+
|
|
2254
2447
|
**Honest scope (stated in the served text AND the result's `honesty`
|
|
2255
2448
|
block):** the live catalog reports a field's SHAPE — existence,
|
|
2256
2449
|
requiredness, type, `maxLength`, read-only, queryable, picklist-ness —
|
|
@@ -3355,6 +3548,11 @@ sentence (`UNTRUSTED_NOTE`, mirrored verbatim in
|
|
|
3355
3548
|
`examples/mcp-register.ts` and `scripts/project-mcp-tools.mjs` so the
|
|
3356
3549
|
served description equals the manifest's), and their WIRE `outputSchema`
|
|
3357
3550
|
carries the field list as the `x-untrusted` extension (zod v4 `.meta`). The
|
|
3551
|
+
cross-resource tools (`resolveAny`, `searchAcrossResources`, the mode tools)
|
|
3552
|
+
carry `["*"]` instead of the union: the per-resource tools already list every
|
|
3553
|
+
field, the union only grows with each added resource, and the served surface has
|
|
3554
|
+
a hard token budget — `["*"]` states the stronger claim ("every text value here is
|
|
3555
|
+
vendor-authored") without repeating the catalog in five schemas. The
|
|
3358
3556
|
reference server (`examples/mcp-register.ts`) wraps the affected fields in
|
|
3359
3557
|
`<untrusted_content>` markers AFTER the SDK call (projection layer only —
|
|
3360
3558
|
the SDK primitives stay raw), strips ONE outer wrapper from write/filter
|
package/examples/mcp/schemas.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as z from 'zod/v4';
|
|
|
6
6
|
import type { StandardSchemaWithJSON } from '@modelcontextprotocol/server';
|
|
7
7
|
import type { CapabilityRecord } from 'node-autotask/capabilities';
|
|
8
8
|
import { WORKFLOW_RESOURCES } from 'node-autotask/operations';
|
|
9
|
-
import {
|
|
9
|
+
import { untrustedFieldsOf } from './manifest.js';
|
|
10
10
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
// Input fragments (zod v4) — the shapes the manifest's `input` column states.
|
|
@@ -109,6 +109,18 @@ export function dataField(description: string, fields: readonly string[]): z.Zod
|
|
|
109
109
|
return fields.length > 0 ? base.meta({ 'x-untrusted': [...fields] }) : base;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/** The cross-resource wildcard result (search_across / resolve_any / a mode
|
|
113
|
+
* tool): its untrusted set is the union over every resource, which grows with
|
|
114
|
+
* each added resource and is already listed field-by-field on that resource's
|
|
115
|
+
* own tools. On the wire it is therefore marked `["*"]` — "every text value in
|
|
116
|
+
* this result is vendor-authored" — which is the stronger claim and does not
|
|
117
|
+
* repeat the catalog in five schemas (the served surface has a hard budget).
|
|
118
|
+
* The runtime redaction still uses the full union (`CROSS_RESOURCE_UNTRUSTED`
|
|
119
|
+
* in tools-mode.ts); only the wire annotation is compacted. */
|
|
120
|
+
export function crossDataField(description: string): z.ZodType {
|
|
121
|
+
return z.unknown().describe(description).meta({ 'x-untrusted': ['*'] });
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
/** One resolved record: `found: false` is a complete answer, not an error.
|
|
113
125
|
* The `record` field carries `x-untrusted` for the tool's resource. */
|
|
114
126
|
export function oneOutput(rec: CapabilityRecord): z.ZodType {
|
|
@@ -139,7 +151,7 @@ export function hitsOutput(): z.ZodType {
|
|
|
139
151
|
return z.object({
|
|
140
152
|
query: z.string(),
|
|
141
153
|
results: z.array(
|
|
142
|
-
z.object({ resource: z.string(), matches: z.array(
|
|
154
|
+
z.object({ resource: z.string(), matches: z.array(crossDataField('Compact summary rows.')) }),
|
|
143
155
|
),
|
|
144
156
|
skipped: z.array(z.string().describe('Requested resources without a search helper — reported, never errored.')),
|
|
145
157
|
});
|
|
@@ -150,10 +162,7 @@ export function resolveAnyOutput(): z.ZodType {
|
|
|
150
162
|
return z.object({
|
|
151
163
|
identifier: z.union([z.string(), z.number()]),
|
|
152
164
|
results: z.array(
|
|
153
|
-
|
|
154
|
-
'Per-resource outcome: value (compact or null), resolutionCost, scanned, candidates/error on a miss.',
|
|
155
|
-
CROSS_RESOURCE_UNTRUSTED,
|
|
156
|
-
),
|
|
165
|
+
crossDataField('Per-resource outcome: value (compact or null), resolutionCost, scanned, candidates/error on a miss.'),
|
|
157
166
|
),
|
|
158
167
|
matches: z.array(z.string().describe('The resources that resolved to exactly one record.')),
|
|
159
168
|
});
|
|
@@ -190,7 +199,7 @@ export function invokeOutput(): z.ZodType {
|
|
|
190
199
|
return z.object({
|
|
191
200
|
operation: z.string(),
|
|
192
201
|
simulated: z.boolean(),
|
|
193
|
-
result:
|
|
202
|
+
result: crossDataField('The SDK result — the pinned DryRunResult envelope for a simulated write.'),
|
|
194
203
|
});
|
|
195
204
|
}
|
|
196
205
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-autotask",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Fully-typed TypeScript SDK for the Autotask (Datto) PSA REST API. Zero runtime dependencies. Built for building MCP servers and integrations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|