iit-mcp-autotask 0.2.0 → 0.4.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/.env.example CHANGED
@@ -75,6 +75,13 @@ AUTOTASK_CACHE_PRESET=on
75
75
  # AUTOTASK_TIMEOUT_MS=20000
76
76
  # AUTOTASK_MAX_RETRIES=1
77
77
 
78
+ # --- output_path (stdio transport only; the one file this server may write) ---
79
+ # The root the bounded query tools' output_path option may write into. UNSET = the option refuses
80
+ # (it is off by default). Absolute, already EXISTING directory only (the server never creates the
81
+ # root) and never the filesystem root; the caller passes a path RELATIVE to it, and a traversal or a
82
+ # symlink escape is refused BEFORE anything is created. See README, Query results.
83
+ # AUTOTASK_OUTPUT_DIR=/srv/autotask-output
84
+
78
85
  # --- Operational ---
79
86
  PORT=8080
80
87
  LOG_LEVEL=info
package/README.md CHANGED
@@ -3,21 +3,21 @@
3
3
  MCP server for the Autotask (Datto) PSA domain, built on the [`node-autotask`](https://www.npmjs.com/package/node-autotask) SDK.
4
4
 
5
5
  It is an **AI-facing capability layer over the SDK** — not a raw SDK passthrough: a minimal, task-oriented
6
- tool surface (38 tools) anchored to ten confirmed MSP workflows, composites that collapse multi-read
6
+ tool surface (41 tools) anchored to ten confirmed MSP workflows, composites that collapse multi-read
7
7
  context into one call, a generic capability layer for the long tail, bounded responses, predictable
8
8
  schemas, strong security boundaries, and predictable per-tenant in-process state (pooled client +
9
- TTL read cache).
9
+ SDK-owned TTL caches).
10
10
 
11
- > **Package status (2026-10-10):** the runtime dependency `node-autotask` is published to public npm
12
- > at `0.8.3` (stricter identifier validation and pagination handling); the committed `pnpm-lock.yaml` pins that
11
+ > **Package status (2026-10-12):** the runtime dependency `node-autotask` is published to public npm
12
+ > at `0.10.0`; the committed `pnpm-lock.yaml` pins that
13
13
  > release with its integrity hash, so installs resolve deterministically. The dependency is always npm
14
- > (node-autotask pinned exactly at `0.8.3`) — never a `file:`, `git:`, `link:` or workspace reference
14
+ > (node-autotask pinned exactly at `0.10.0`) — never a `file:`, `git:`, `link:` or workspace reference
15
15
  > to a local checkout.
16
16
 
17
17
  ## Tool surface
18
18
 
19
- 38 tools: **12 platform/discovery** (Layer A) + **5 workflow-resource resolves** (Layer B) +
20
- **21 dedicated workflow tools** (Layer C). Every other operation in the SDK's 1,212-operation
19
+ 41 tools: **13 platform/discovery** (Layer A) + **5 workflow-resource resolves** (Layer B) +
20
+ **23 dedicated workflow tools** (Layer C). Every other operation in the SDK's 1,212-operation
21
21
  registry stays reachable through the **generic capability layer** — never pre-registered.
22
22
 
23
23
  Composites bound **each part independently**: every part reads its own declared limit argument
@@ -27,9 +27,9 @@ miss never fails the call (typed `absent`); only a missing root record fails the
27
27
 
28
28
  | Layer | Tools | Role |
29
29
  |-------|-------|------|
30
- | A — platform & discovery (12) | `autotask_catalog` · `autotask_describe` · `autotask_read` · `autotask_write` · `autotask_delete` · `autotask_resource_who_am_i` · `autotask_version` · `autotask_threshold_information` · `autotask_resolve_any` · `autotask_search_across_resources` · `autotask_fetch_many` · `autotask_relationship_map` | The five **META** dispatchers + tenant identity + cross-resource helpers |
30
+ | A — platform & discovery (13) | `autotask_catalog` · `autotask_describe` · `autotask_read` · `autotask_write` · `autotask_delete` · `autotask_resource_who_am_i` · `autotask_version` · `autotask_threshold_information` · `autotask_resolve_any` · `autotask_search_across_resources` · `autotask_fetch_many` · `autotask_read_batch` · `autotask_relationship_map` | The five **META** dispatchers + tenant identity + cross-resource helpers |
31
31
  | B — workflow resolves (5) | `autotask_company_resolve` · `autotask_configurationItems_resolve` · `autotask_contact_resolve` · `autotask_project_resolve` · `autotask_ticket_resolve` | Numeric-id-or-exact-name resolution for the five SDK workflow resources |
32
- | C — dedicated workflow (21) | `autotask_ticket_query` · `autotask_ticket_full_context` · `autotask_ticket_sla_health_check` · `autotask_ticket_find_by_ticket_number` · `autotask_company_full_profile` · `autotask_timeEntry_query` · `autotask_timeEntry_create` · `autotask_quote_get` · `autotask_invoice_get` · `autotask_invoice_markup` · `autotask_configurationItem_profile` · `autotask_configurationItems_find_by_serial` · `autotask_contact_full_profile` · `autotask_contact_find_by_email` · `autotask_project_get_context` · `autotask_serviceCall_query` · `autotask_serviceCall_full_profile` · `autotask_contract_full_profile` · `autotask_contract_query` · `autotask_resource_workload` · `autotask_timeOffRequest_query` (names verbatim from the SDK manifest where they exist — case inconsistency included) | Bounded filtered lists, one-call composites, natural-key lookups, the single dedicated write |
32
+ | C — dedicated workflow (23) | `autotask_ticket_query` · `autotask_ticket_full_context` · `autotask_ticket_sla_health_check` · `autotask_ticket_find_by_ticket_number` · `autotask_company_full_profile` · `autotask_timeEntry_query` · `autotask_timeEntry_create` · `autotask_quote_get` · `autotask_invoice_get` · `autotask_invoice_markup` · `autotask_configurationItem_profile` · `autotask_configurationItems_find_by_serial` · `autotask_contact_full_profile` · `autotask_contact_find_by_email` · `autotask_project_get_context` · `autotask_serviceCall_query` · `autotask_serviceCall_full_profile` · `autotask_contract_full_profile` · `autotask_contract_query` · `autotask_resource_workload` · `autotask_timeOffRequest_query` · `autotask_company_unbilled_work` · `autotask_aggregate` (names verbatim from the SDK manifest where they exist — case inconsistency included) | Bounded filtered lists, one-call composites, natural-key lookups, the single dedicated write |
33
33
 
34
34
  Machine-readable catalogue: [`capabilities.json`](capabilities.json) · server metadata: [`server.json`](server.json) ·
35
35
  per-workflow mapping: [`docs/capability-matrix.md`](docs/capability-matrix.md) · full design: [`docs/architecture.md`](docs/architecture.md).
@@ -39,7 +39,7 @@ per-workflow mapping: [`docs/capability-matrix.md`](docs/capability-matrix.md)
39
39
  The five META tools are the SDK's own live-verified governance layer, adopted verbatim (a recorded
40
40
  decision — see `docs/architecture.md` §2.5):
41
41
 
42
- - `autotask_catalog` — list registry operations (bounded: limit default 40 / max 500, tier + effect filters).
42
+ - `autotask_catalog` — list registry operations (bounded: limit default 41 / max 500, tier + effect + `q` (key+purpose substring) + `resource` filters; `compact` reduced rows). Policy-refused writes are hidden by default (the output carries `hidden_refused: N`); `show_refused: true` lists them with their `policyReason`.
43
43
  - `autotask_describe` — exact input/output schema + examples + error codes for one operation by key.
44
44
  - `autotask_read` — route any `effect=read` operation by **exact registry key** (the 363 metadata-trio
45
45
  reads with no dedicated tool ride here). Read-only by construction; a write/delete routed here is
@@ -57,8 +57,15 @@ typed error naming the key and the valid keys. So the ~1,145 non-exposed operati
57
57
  discoverable (`catalog`/`describe`) and reachable (`read`/`write`/`delete`) — nothing is a silent gap.
58
58
  `autotask_catalog` and `autotask_describe` also surface this deployment's policy verdict
59
59
  (`reachable` / `policyReason`) per row, so a policy refusal is visible in the catalog rather than
60
- discovered by failing; the full served descriptions (the single source of truth,
61
- `src/tools/catalog-data.ts`) are mirrored in [`capabilities.json`](capabilities.json).
60
+ discovered by failing; under a restrictive policy the catalog hides the refused
61
+ write/destructive rows by default (the output carries `hidden_refused: N`; `show_refused: true`
62
+ restores them with their `policyReason`) — read refusals (incl. the operator's
63
+ `AUTOTASK_SECRET_READS`) are never hidden; the full served descriptions (the single source of
64
+ truth, `src/tools/catalog-data.ts`) are mirrored in [`capabilities.json`](capabilities.json).
65
+ Every served tool description is capped at ~300 chars; the shared discovery boilerplate (the
66
+ untrusted-content notice, the closed-argument contract, the start-here recipes, the query-option
67
+ guidance) rides the MCP `instructions` served on `initialize` instead of repeating in each
68
+ description — the per-result `<untrusted_content>` markers are unchanged.
62
69
 
63
70
  ### Writes
64
71
 
@@ -78,6 +85,61 @@ token the SDK's redaction deliberately exempts); the tool stays registered and i
78
85
  reachable in this deployment — never *how*: validation, forced dry-run and `confirm` governance
79
86
  stay in the SDK.
80
87
 
88
+ ### Query results: TSV, `output_path` and the saved-file shape
89
+
90
+ The five bounded query tools (`autotask_ticket_query`, `autotask_timeEntry_query`,
91
+ `autotask_serviceCall_query`, `autotask_contract_query`, `autotask_timeOffRequest_query`) answer
92
+ `structuredContent.result` with the SDK's bounded-query envelope:
93
+
94
+ ```
95
+ { items, fetched, hasMore, pagesWalked } // + blindSurface / emptyReason / noteCharsTruncated when stamped
96
+ ```
97
+
98
+ `items` holds the vendor records (free text wrapped in `<untrusted_content>` markers), `fetched` is
99
+ `items.length`, `hasMore` flags a truncated walk — the `limit` or the 1000-page guard on a normal
100
+ query, or the `output_path` 10,000-row cap — and `pagesWalked` counts the pages fetched. `count` is never served — the vendor documents it as the CURRENT page's
101
+ count, so a total rides `<resource>.count`.
102
+
103
+ **Saved files are the host's job.** Unless you ask for `output_path` (below), this server never
104
+ writes a result file on its own: when a result is too large, the MCP host persists the oversized
105
+ tool result, and the file it writes holds this same envelope as JSON — so a saved result can be read
106
+ back with exactly the fields above.
107
+
108
+ Three per-call options change the result form (the detail rides the MCP `instructions`, served on
109
+ `initialize`; the capped descriptions only point there):
110
+
111
+ | Option | Effect |
112
+ |--------|--------|
113
+ | `format: 'tsv'` | serves the table as `result.tsv` — a header row of column names plus one tab-separated line per record — with `result.columns` and `result.rowCount`, and **omits `result.items`** (that omission is the size saving; the JSON form stays the default). Inside a value a literal backslash is written as `\\`, a tab as `\t`, a newline as `\n` and a carriage return as `\r`; the `<untrusted_content>` markers survive inside cells. |
114
+ | `untrusted_wrapper: false` | skips the `<untrusted_content>` wrapping for that call only; the result then carries `untrustedWrapper: 'disabled'` and the human text says so. Absent (the default) wraps exactly as before. |
115
+ | `output_path: '<relative path>'` | **stdio transport only**: walks the FULL match set with the resource's own query iterator, writes the envelope JSON to a file under `AUTOTASK_OUTPUT_DIR`, and returns a summary (`outputPath`, `columns`, `rowCount`, the first rows, `bytes`) instead of the table. |
116
+
117
+ `output_path` is deliberately strict: the deployment must set an **absolute** `AUTOTASK_OUTPUT_DIR`
118
+ that already exists and is not the filesystem root (the feature refuses when it is unset or missing —
119
+ the server never creates the root), and the value must be a path **relative** to that root that names
120
+ a file (a trailing separator, an absolute path, a NUL byte, and a target that is an existing
121
+ directory are all refused). **Nothing outside the root is ever created,
122
+ file or directory**: the deepest existing ancestor of the target is canonicalised (`realpath`) and
123
+ checked against the resolved root *before* any directory is created, so a symlinked parent — even
124
+ with a deep sub-path — is refused with zero filesystem effect; only then are the missing directories
125
+ made and the resolved parent re-verified. The **write itself re-enforces both rules at write time**
126
+ (validation and the write are separate steps): it re-resolves the parent — which must still be inside
127
+ the root — refuses a target that is not a regular file, and opens with `O_NOFOLLOW`, so a symlink
128
+ swapped in after validation is never followed. A dangling symlinked parent is the same typed refusal,
129
+ and every filesystem failure crosses as that typed refusal naming the error code, never an OS message
130
+ (those embed the caller's path). The root itself may be a symlink — containment is then evaluated
131
+ against its canonical target. Under stdio the caller already owns the filesystem, so this is
132
+ containment (a caller path can never make the server create or write outside the root), not a defence
133
+ against a same-user process racing the server. One literal case the containment statement does not
134
+ cover: a hard link placed at the validated path is a regular file to `lstat`, and `O_NOFOLLOW` does not
135
+ apply to it, so the write can land on an inode that is also named outside the root — hard links cannot
136
+ be detected portably, so there is no portable fix.
137
+
138
+ The walk stops at 10,000 rows with an explicit truncation note in the summary (`hasMore: true` then
139
+ means the cap, not only the query limit). The call is refused outright on the `http` transport (the
140
+ caller is a request, not the process owner), and it cannot be combined with `format`, `latest` or
141
+ `note_chars` — one result form per call.
142
+
81
143
  ## Transports
82
144
 
83
145
  | Transport | For | Caller auth | Autotask credential |
@@ -176,7 +238,10 @@ non-zero on an invalid config (fail-fast) instead of failing at the first tool c
176
238
  | `AUTOTASK_WRITE_POLICY` | no | `deny` | `deny` / `allow_list` / `all` |
177
239
  | `AUTOTASK_WRITE_ALLOW` | only under `allow_list` | — | comma-separated registry operation keys; boot fails on empty/unknown keys |
178
240
  | `AUTOTASK_SECRET_READS` | no | `deny` | `deny` refuses `authenticate.get` (plaintext vendor token) — the gate is scoped to the **whole `authenticate` resource**, so any future `authenticate.*` operation arrives denied, not reachable; `allow` permits it — the token is still never logged |
179
- | `AUTOTASK_CACHE_PRESET` | no | `on` | per-tenant in-process TTL read cache (see [Per-tenant pool & cache](#per-tenant-pool--cache)); `off` disables it (every read re-dials the vendor) |
241
+ | `AUTOTASK_CACHE_PRESET` | no | `on` | per-tenant in-process caches (see [Per-tenant pool & cache](#per-tenant-pool--cache)): `on` (default) sends no cache blocks — the SDK's default-on caches apply to each pooled tenant client (read ~5 min + the plan-derived reference table, field ~5 min, resource-name ~1 h); `off` opts every SDK cache channel out explicitly (every read re-dials the vendor) |
242
+ | `AUTOTASK_COMPOSITE_NAMES` | no | `off` | opt-in name/label siblings on the four SDK-backed composites (`tickets.fullContext`, `companies.fullProfile`, `configuration_items.profile`, `projects.getContext`) — reuses the SDK's existing caches (no new cache); `off` = byte-identical output to today; `on` adds the name siblings + the top-level `names` section, bounded by the SDK name-read budget (default 20 cold wire reads, lowerable only) |
243
+ | `AUTOTASK_FIELD_CACHE_TTL_MS` | no | unset (SDK 5 min) | pass-through to the SDK's field-metadata cache TTL (positive integer ms), sent as the scalar `fieldCache` block on the `on` preset path only — the server holds no TTL value of its own: unset or invalid (non-integer, <= 0) sends no `fieldCache` block and the SDK default (5 min) applies; `off` keeps its explicit opt-out; `fieldCatalog` `fresh: true` still bypasses the cache |
244
+ | `AUTOTASK_OUTPUT_DIR` | no | unset | the ONE directory the bounded query tools' `output_path` option may write into (stdio transport only). Unset = the feature refuses; must be absolute, must already EXIST (the server never creates the root) and must not be the filesystem root. A caller path is relative to this root, and the root and target are canonically resolved (`realpath`) so a traversal or symlink escape is refused — with nothing created outside the root. See [Query results](#query-results-tsv-output_path-and-the-saved-file-shape) |
180
245
  | `AUTOTASK_RATE_LIMIT_PER_HOUR` | no | 8000 (agent profile) | override the SDK's local hourly budget (never above the vendor cap; `null`/off is rejected at boot — it would void the budget guard) |
181
246
  | `AUTOTASK_MAX_CONCURRENT` | no | 3 (agent profile) | override the SDK's local concurrency bound |
182
247
  | `AUTOTASK_TIMEOUT_MS` | no | 20000 (agent profile) | whole-call budget |
@@ -334,11 +399,12 @@ guards (rate limiter, 429 cooldown) — a worse security posture, not a safer on
334
399
  (user, integration code, secret, resolved base URL, impersonation id). Optional idle TTL (lazy,
335
400
  no timers). Eviction is logged without values. A vendor `401` (or an impersonation-scope `403`)
336
401
  deletes the entry (no negative caching).
337
- - **Cache** — `AUTOTASK_CACHE_PRESET=on` (default) applies the SDK's reference TTL table
338
- (`node-autotask/src/cache-ttl-tables.ts`) to each pooled tenant client: structural records ~1 h,
339
- mutable records ~5 min, base records ~1 min, plus the `fieldCache` field-metadata channel.
402
+ - **Cache** — `AUTOTASK_CACHE_PRESET=on` (default) sends no cache blocks — the SDK's default-on
403
+ caches apply to each pooled tenant client (the reference TTL table moved into the SDK with
404
+ 0.9.0's default-on change: structural ~1 h / mutable ~5 min, field ~5 min, resource-name ~1 h).
340
405
  Repeated reads of the same record are served from that tenant's cache until TTL expiry; composites
341
- de-duplicate their 3–8 fan-out reads through it. `off` disables it (every read re-dials the vendor).
406
+ de-duplicate their 3–8 fan-out reads through it. `off` opts every SDK cache channel out explicitly
407
+ (every read re-dials the vendor).
342
408
  - **Boundaries — never relaxed:** in-process only (never on disk), **per-tenant** (one tenant's
343
409
  cached records or limiter state can never answer another tenant's request), TTL-bounded, cleared
344
410
  on eviction and on `401`. The same boundary covers the limiter/cooldown state.
@@ -381,7 +447,7 @@ Full control set: `.squad/security.md` (S1–S13, blocking B1–B9, decisions D-
381
447
 
382
448
  ## SDK dependency
383
449
 
384
- `iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.8.3`** (an exact pin —
450
+ `iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.10.0`** (an exact pin —
385
451
  no range operator; subpaths `.` / `/capabilities` / `/operations` / `/resources` / `/types` /
386
452
  `/pool`) — the only **vendor** dependency; the other two runtime dependencies are
387
453
  `@modelcontextprotocol/server` (the MCP framework) and `zod` (schema validation). It never
@@ -391,7 +457,7 @@ user — the published tarball ships no lockfile) must resolve to the release th
391
457
  reviewed against, or the plan-hash boot check refuses to start.
392
458
 
393
459
  **Publishing `node-autotask` is the maintainer's action, done manually.** It is published on the
394
- public registry at `0.8.3` (published 2026-10-10: stricter identifier validation and pagination handling); the
460
+ public registry at `0.10.0` (published 2026-10-11); the
395
461
  exact pin and the committed `pnpm-lock.yaml` (with `pnpm-workspace.yaml`) pin the real published
396
462
  tarball with its integrity hash, so installs resolve to exactly that release — the lockfile
397
463
  freezes the pnpm/git/Docker paths, the pin freezes the npm/npx paths. The docs are written against this