iit-mcp-autotask 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/README.md +80 -14
- package/capabilities.json +84 -42
- package/dist/config.d.ts +6 -0
- package/dist/config.js +38 -4
- package/dist/mocks/fixture-ops.d.ts +22 -3
- package/dist/mocks/fixture-ops.js +26 -0
- package/dist/mocks/fixtures.js +1 -1
- package/dist/mocks/harness.js +4 -3
- package/dist/tools/catalog-data.d.ts +42 -41
- package/dist/tools/catalog-data.js +88 -89
- package/dist/tools/composites.d.ts +1 -1
- package/dist/tools/composites.js +98 -21
- package/dist/tools/context.d.ts +50 -1
- package/dist/tools/context.js +42 -7
- package/dist/tools/dispatch.d.ts +33 -0
- package/dist/tools/dispatch.js +65 -19
- package/dist/tools/lists.js +82 -3
- package/dist/tools/meta.d.ts +4 -1
- package/dist/tools/meta.js +150 -16
- package/dist/tools/output-file.d.ts +39 -0
- package/dist/tools/output-file.js +222 -0
- package/dist/tools/register.d.ts +8 -7
- package/dist/tools/register.js +3 -3
- package/dist/tools/resolves.d.ts +6 -0
- package/dist/tools/resolves.js +53 -12
- package/dist/tools/results.d.ts +10 -0
- package/dist/tools/results.js +28 -17
- package/dist/tools/schemas.d.ts +247 -14
- package/dist/tools/schemas.js +434 -21
- package/dist/tools/server.d.ts +16 -2
- package/dist/tools/server.js +72 -2
- package/dist/tools/tsv.d.ts +43 -0
- package/dist/tools/tsv.js +84 -0
- package/package.json +1 -1
- package/server.json +1 -1
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,10 +3,10 @@
|
|
|
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 (
|
|
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
|
|
9
|
+
SDK-owned TTL caches).
|
|
10
10
|
|
|
11
11
|
> **Package status (2026-10-12):** the runtime dependency `node-autotask` is published to public npm
|
|
12
12
|
> at `0.10.0`; the committed `pnpm-lock.yaml` pins that
|
|
@@ -16,8 +16,8 @@ TTL read cache).
|
|
|
16
16
|
|
|
17
17
|
## Tool surface
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
**
|
|
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 (
|
|
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 (
|
|
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
|
|
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;
|
|
61
|
-
|
|
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
|
|
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)
|
|
338
|
-
|
|
339
|
-
|
|
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`
|
|
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.
|