iit-mcp-autotask 0.38.8 → 0.40.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 +10 -11
- package/README.md +57 -71
- package/capabilities.json +27 -10
- package/dist/access/gate.d.ts +1 -2
- package/dist/access/gate.js +4 -18
- package/dist/access/write-env.d.ts +5 -10
- package/dist/access/write-env.js +20 -46
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +1 -1
- package/dist/config/schema.d.ts +4 -11
- package/dist/config/schema.js +10 -27
- package/dist/doctor.d.ts +1 -3
- package/dist/http.d.ts +1 -1
- package/dist/http.js +1 -1
- package/dist/main.js +0 -7
- package/dist/tools/catalog-data.d.ts +11 -20
- package/dist/tools/catalog-data.js +22 -22
- package/dist/tools/context/client.d.ts +2 -4
- package/dist/tools/context/client.js +15 -15
- package/dist/tools/context/types.d.ts +1 -1
- package/dist/tools/dispatch.js +2 -23
- package/dist/tools/lists.js +16 -28
- package/dist/tools/meta.js +7 -9
- package/dist/tools/reasons.d.ts +1 -1
- package/dist/tools/reasons.js +8 -3
- package/dist/tools/register.d.ts +1 -1
- package/dist/tools/register.js +0 -2
- package/dist/tools/results/envelope.d.ts +5 -5
- package/dist/tools/results/envelope.js +24 -3
- package/dist/tools/results/errors.js +15 -7
- package/dist/tools/results/untrusted.js +3 -2
- package/dist/tools/revenue-rollup.d.ts +3 -0
- package/dist/tools/revenue-rollup.js +35 -0
- package/dist/tools/schemas.d.ts +36 -66
- package/dist/tools/schemas.js +42 -60
- package/dist/tools/server.d.ts +8 -7
- package/dist/tools/server.js +23 -26
- package/dist/tools/surface.js +5 -5
- package/dist/tools/write/apply.d.ts +11 -2
- package/dist/tools/write/apply.js +385 -23
- package/dist/tools/write/facts.d.ts +2 -6
- package/dist/tools/write/facts.js +12 -5
- package/dist/tools/write/outcome.d.ts +1 -1
- package/dist/tools/write/outcome.js +2 -6
- package/dist/tools/write/preview.js +98 -147
- package/dist/tools/write/snapshot.d.ts +1 -2
- package/dist/tools/write/snapshot.js +0 -3
- package/dist/tools/write.d.ts +3 -24
- package/dist/tools/write.js +3 -24
- package/dist/write/canonical.d.ts +7 -5
- package/dist/write/canonical.js +3 -6
- package/dist/write/evaluate.d.ts +36 -5
- package/dist/write/evaluate.js +62 -11
- package/dist/write/runtime.d.ts +0 -2
- package/dist/write/runtime.js +4 -5
- package/dist/write/store/audit-log.d.ts +2 -2
- package/dist/write/store/audit-log.js +6 -36
- package/dist/write/store/constants.d.ts +2 -2
- package/dist/write/store/constants.js +2 -2
- package/dist/write/store/ledger.d.ts +3 -6
- package/dist/write/store/ledger.js +5 -59
- package/dist/write/store/lifecycle.d.ts +1 -1
- package/dist/write/store/lifecycle.js +1 -1
- package/dist/write/store/plan-store.d.ts +1 -1
- package/dist/write/store/plan-store.js +2 -28
- package/dist/write/store/types.d.ts +9 -64
- package/dist/write/store.d.ts +2 -4
- package/dist/write/store.js +5 -13
- package/examples/agentgateway.write-gate.yaml +2 -2
- package/package.json +4 -3
- package/server.json +3 -3
- package/skills/contract-profitability.md +2 -1
- package/skills/crm-management.md +5 -5
- package/skills/log-time-entry.md +8 -9
- package/skills/revenue-rollup.md +62 -0
- package/dist/moves.d.ts +0 -17
- package/dist/moves.js +0 -120
- package/dist/tools/write/apply-phases.d.ts +0 -107
- package/dist/tools/write/apply-phases.js +0 -421
- package/dist/write/store/audit-chain.d.ts +0 -5
- package/dist/write/store/audit-chain.js +0 -23
- package/dist/write/store/unresolved.d.ts +0 -9
- package/dist/write/store/unresolved.js +0 -45
package/.env.example
CHANGED
|
@@ -52,13 +52,11 @@ MCP_TRANSPORT=stdio
|
|
|
52
52
|
# readonly (default): reads only, no state. operator: adds autotask_write (preview) and
|
|
53
53
|
# autotask_apply (execute the previewed plan), governed by AUTOTASK_WRITE; needs the state dir and run scope below.
|
|
54
54
|
AUTOTASK_PROFILE=readonly
|
|
55
|
-
# operator: which writes are served: allow
|
|
56
|
-
#
|
|
57
|
-
# AUTOTASK_WRITE=*:
|
|
55
|
+
# operator: which writes are served: allow or deny per operation; unlisted is denied.
|
|
56
|
+
# Hard rules require human confirmation. Grammar and dedup: docs/write-gate.md.
|
|
57
|
+
# AUTOTASK_WRITE=*:deny,tickets.*:allow,time_entries.create:allow,companies.update:deny
|
|
58
58
|
# AUTOTASK_DEDUP=companies.create:companyName
|
|
59
|
-
#
|
|
60
|
-
# AUTOTASK_TRUSTED_HOST_PROMPT=stdio
|
|
61
|
-
# operator: the state dir (mode 0700; plans, ledger, audit log, lock; one process per dir).
|
|
59
|
+
# operator: the state dir (mode 0700; plans, ledger, audit, locks; same-version processes on one host may share it).
|
|
62
60
|
# AUTOTASK_STATE_DIR=/var/lib/autotask-mcp
|
|
63
61
|
# operator on stdio: the run scope (required), {"companyIds":[...]} or {"all":true} (any
|
|
64
62
|
# company). On HTTP set runScope per token instead.
|
|
@@ -68,7 +66,7 @@ AUTOTASK_PROFILE=readonly
|
|
|
68
66
|
# The kill file: while it exists every preview and apply is denied (default <state dir>/KILL).
|
|
69
67
|
# AUTOTASK_KILL_FILE=/var/lib/autotask-mcp/KILL
|
|
70
68
|
# Optional toolsets on top of core (core is always on): a comma list of service, records, finance,
|
|
71
|
-
# or all. An unknown name fails boot. Unset = core only
|
|
69
|
+
# or all. An unknown name fails boot. Unset = core only.
|
|
72
70
|
# AUTOTASK_TOOLSETS=service,records,finance
|
|
73
71
|
# IANA time zone of the people asking (their local "today"), default UTC; not the tenant's or
|
|
74
72
|
# the server's zone. Autotask stores UTC; the zone is reported by
|
|
@@ -81,10 +79,11 @@ AUTOTASK_PROFILE=readonly
|
|
|
81
79
|
# AUTOTASK_SECRET_READS. authenticate.* (a credential-minting read) is always refused.
|
|
82
80
|
|
|
83
81
|
# --- Per-tenant read cache (in-process; never cross-tenant; never on disk) ---
|
|
84
|
-
# on (default):
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
|
|
82
|
+
# on (default): the SDK's default-on caches apply to each pooled tenant client (read ~5 min,
|
|
83
|
+
# reference tables ~1 h, field metadata ~5 min); repeated reads of the same record are served
|
|
84
|
+
# from the tenant's cache until TTL expiry. off: `cache: false` is sent — every read re-dials
|
|
85
|
+
# the vendor.
|
|
86
|
+
AUTOTASK_CACHE=on
|
|
88
87
|
|
|
89
88
|
# --- Vendor budget (the server boots the SDK in the conservative `agent` profile; overrides here) ---
|
|
90
89
|
# AUTOTASK_RATE_LIMIT_PER_HOUR=8000
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ client + SDK-owned TTL caches).
|
|
|
11
11
|
## Tool surface
|
|
12
12
|
|
|
13
13
|
The **core** toolset (10 tools) is always served; `AUTOTASK_TOOLSETS` adds `service`, `records` and
|
|
14
|
-
`finance` (or `all`,
|
|
14
|
+
`finance` (or `all`, 18 tools); the operator profile adds two. The whole tool surface (20 tools) is
|
|
15
15
|
listed below. Every other operation in the SDK's operation registry stays
|
|
16
16
|
reachable through the generic layer: `autotask_catalog` finds the key, `autotask_describe` serves
|
|
17
17
|
its contract (slim by default: transport and retry detail dropped, long text cut, the first two
|
|
@@ -22,7 +22,7 @@ examples; `full: true` serves the complete record, `summary: true` about 2 KB),
|
|
|
22
22
|
| core (always) | `autotask_guide` · `autotask_catalog` · `autotask_describe` · `autotask_read` · `autotask_resolve` · `autotask_query` · `autotask_tenant_status` · `autotask_ticket_full_context` · `autotask_company_full_profile` · `autotask_resource_workload` |
|
|
23
23
|
| `service` | `autotask_project_get_context` · `autotask_service_call_full_profile` |
|
|
24
24
|
| `records` | `autotask_contact_full_profile` · `autotask_configuration_item_profile` · `autotask_contract_full_profile` |
|
|
25
|
-
| `finance` | `autotask_quote_get` · `autotask_invoice_get` (`format`: `record`, `html`, `xml`, `pdf`) |
|
|
25
|
+
| `finance` | `autotask_quote_get` · `autotask_invoice_get` (`format`: `record`, `html`, `xml`, `pdf`) · `autotask_revenue_rollup` |
|
|
26
26
|
| `operator` (operator profile) | `autotask_write` (preview, changes nothing) · `autotask_apply` (execute the previewed plan) |
|
|
27
27
|
|
|
28
28
|
- `autotask_resolve` turns any identifier into a record: an id or name, `by: "ticket_number"`,
|
|
@@ -81,7 +81,7 @@ vendor, so loading a playbook costs no wire and adds no injection surface.
|
|
|
81
81
|
- `{skill, section}`: one of its `## ` sections.
|
|
82
82
|
|
|
83
83
|
Skills: `ticket-triage`, `company-360`, `unbilled-work-review`, `contract-profitability`,
|
|
84
|
-
`invoice-export`, `filter-semantics`, `slim-reads`, and on the operator profile `log-time-entry` and `crm-management`.
|
|
84
|
+
`revenue-rollup`, `invoice-export`, `filter-semantics`, `slim-reads`, and on the operator profile `log-time-entry` and `crm-management`.
|
|
85
85
|
|
|
86
86
|
The playbooks are discoverable in both directions:
|
|
87
87
|
|
|
@@ -100,6 +100,7 @@ part miss never fails the call (typed `absent`); only a missing root record fail
|
|
|
100
100
|
{ status: ok|needs_input|ambiguous|denied|error,
|
|
101
101
|
data, next: proceed|supply_input|choose_candidate|...|retry|stop,
|
|
102
102
|
reasons: [{ code, field?, detail }], retryable, correlationId, summary }
|
|
103
|
+
// status ok omits next, reasons, retryable and correlationId
|
|
103
104
|
```
|
|
104
105
|
|
|
105
106
|
`isError` is set exactly for `denied` and `error`; on an error `data` is the typed error payload
|
|
@@ -143,7 +144,7 @@ warning naming any unrecognised `AUTOTASK_*` variable at boot), so the profile g
|
|
|
143
144
|
|
|
144
145
|
### Operator profile (writes)
|
|
145
146
|
|
|
146
|
-
Writes are two calls. `autotask_write {operation, args, reason
|
|
147
|
+
Writes are two calls. `autotask_write {operation, args, reason?}` previews: it
|
|
147
148
|
reads the target live, runs the SDK dry run, evaluates the policy and returns a decision, a diff and,
|
|
148
149
|
when allowed, a plan handle (`pln_...`, valid 15 minutes). Nothing is written. `autotask_apply
|
|
149
150
|
{plan}` executes exactly that plan, after checking that nothing it depends on has changed. Targets
|
|
@@ -155,44 +156,28 @@ An operator deployment needs:
|
|
|
155
156
|
| Variable | Meaning |
|
|
156
157
|
|----------|---------|
|
|
157
158
|
| `AUTOTASK_PROFILE=operator` (or an operator token) | the profile |
|
|
158
|
-
| `AUTOTASK_WRITE` | which writes are served and how: `allow
|
|
159
|
-
| `AUTOTASK_STATE_DIR` | mode 0700; holds encrypted plans, the idempotency ledger, the
|
|
159
|
+
| `AUTOTASK_WRITE` | which writes are served and how: `allow` or `deny` per operation, unlisted is read-only (grammar in [docs/write-gate.md](docs/write-gate.md)) |
|
|
160
|
+
| `AUTOTASK_STATE_DIR` | mode 0700; holds encrypted plans, the idempotency ledger, the append-only JSONL audit log, the primary lock and the holder registry (many processes may share one dir — see below) |
|
|
160
161
|
| `AUTOTASK_RUN_SCOPE` | `{"companyIds":[...]}` or `{"all":true}`: the run scope on stdio and in gateway mode (one scope shared by every gateway principal). In tokens mode each operator token carries `runScope` instead |
|
|
161
162
|
| `AUTOTASK_PRINCIPAL` | stdio: the name plans and the audit log are bound to (default `stdio`) |
|
|
162
163
|
| `AUTOTASK_KILL_FILE` | optional: while this file exists every preview and apply is denied (default `<state dir>/KILL`) |
|
|
163
164
|
|
|
164
165
|
What the write access can and cannot do (full reference: [docs/write-gate.md](docs/write-gate.md)):
|
|
165
166
|
|
|
166
|
-
- `AUTOTASK_WRITE` lists operations, families (`tickets.*`) or `*`, each `allow`
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
in a terminal, as the server's OS user, with `AUTOTASK_STATE_DIR` set, `autotask-mcp moves list` shows
|
|
181
|
-
the blocked sources and `autotask-mcp moves resolve <resource>:<id>` (after you checked both tickets in
|
|
182
|
-
Autotask; you type the source id, and the copy's id when the ledger names one) closes the ledger lines
|
|
183
|
-
as `MOVE_RESOLVED` and audits it (`move_resolved`). It refuses within 10 minutes of the move being sent
|
|
184
|
-
and, while the engine may still run, when the sending server is still registered or alive. A plan
|
|
185
|
-
previewed before the resolve cannot apply after it. It is not an MCP tool, and it is a guardrail, not a
|
|
186
|
-
boundary against an agent with a same-user shell. Billed, attached, approved, closed-target and
|
|
187
|
-
older-than-14-days sources are refused.
|
|
188
|
-
- Hard rules no setting overrides: `authenticate.*` is never served; operations the SDK flags
|
|
189
|
-
`requiresApproval` or sensitive (webhooks, `resources.transferOwnership`, `contacts.moveToCompany`, deletes) can only
|
|
190
|
-
be `gate` or `deny`. An exact `allow` entry for one is a boot error; a family or `*` entry resolves it
|
|
191
|
-
to `gate`. Destructive and move operations can be `allow`ed (see the trust model in the doc).
|
|
192
|
-
- When a person must finish a job by hand (a move whose source delete the vendor refused), the result is
|
|
193
|
-
`next: manual_action` and `data.move.manualAction` carries what to do and, when the pinned SDK
|
|
194
|
-
provides `operations.uiLink`, a clickable Autotask deep link. There is no `autotask_delete` (D-12):
|
|
195
|
-
deletes preview through `autotask_write`.
|
|
167
|
+
- `AUTOTASK_WRITE` lists operations, families (`tickets.*`) or `*`, each `allow` or `deny`.
|
|
168
|
+
Unlisted writes are `OP_NOT_ALLOWED`. `AUTOTASK_DEDUP` names duplicate-check fields for creates.
|
|
169
|
+
A bad entry fails boot; the former `gate` mode is refused.
|
|
170
|
+
- Hard rules still require a person for SDK `requiresApproval` or sensitive operations, and moves
|
|
171
|
+
across companies or contracts. The server asks through MCP form elicitation; a client without
|
|
172
|
+
form support gets `GATE_UNSUPPORTED`, and decline or dismissal gives `GATE_DECLINED`.
|
|
173
|
+
A gateway client id or host permission prompt never bypasses this check.
|
|
174
|
+
- Move a time entry or ticket charge with `time_entries.moveToTicket` or `ticket_charges.moveToTicket`,
|
|
175
|
+
args `[sourceId, toTicketId]`. The SDK copies, verifies, then deletes the source; the new record
|
|
176
|
+
gets a new id and history, and notifications may fire. Billed, attached, approved, closed-target
|
|
177
|
+
and old sources are refused by the SDK. Follow `data.move.manualAction` when a person must delete
|
|
178
|
+
the source. A partial or unknown move is never blindly retried: check both tickets in Autotask.
|
|
179
|
+
The server retains write-ahead/replay protection, but no separate move recovery snapshot or
|
|
180
|
+
`moves list/resolve` command. Deletes preview through `autotask_write`.
|
|
196
181
|
- The company scope is read from the live records the write touches (the target, its parent, the
|
|
197
182
|
ticket, project, task or contract it references, and any `companyID` it sets), never from names
|
|
198
183
|
or titles. A reference that cannot be read is refused.
|
|
@@ -205,19 +190,24 @@ the preview diff shows exactly what is sent. A string that still carries a combi
|
|
|
205
190
|
precomposed form after NFC (for example a lone U+0301) is refused `INPUT_INVALID`: send the
|
|
206
191
|
precomposed character.
|
|
207
192
|
|
|
208
|
-
Outcomes the caller acts on: `ok
|
|
193
|
+
Outcomes the caller acts on: `ok`; `needs_input`/`supply_input` (`missing` names the
|
|
209
194
|
field); `denied`/`stop`; `STATE_DRIFT` or `PLAN_EXPIRED` →
|
|
210
195
|
`repreview`; a failed write: `WRITE_FAILED` with `stop` (a deterministic rejection; it replays,
|
|
211
|
-
fix the input and
|
|
196
|
+
fix the input and preview the corrected payload) or `RATE_LIMITED` with `retry` (nothing was written: apply the
|
|
212
197
|
same plan again after the wait); a vendor that accepted an update but kept the old value (`FIELD_NOT_PERSISTED` with `stop`: the apply reads the stored record back and compares every field you sent; the write itself is `succeeded`, `data.record` shows what is stored and `data.notPersisted` names the fields — the apply re-reads twice, after waiting 0.75 s, then a further 1.5 s, before concluding, because the vendor read path can lag its write; re-read the record before repeating the update; a replay answers the same); an unknown outcome (`WRITE_OUTCOME_UNKNOWN`,
|
|
213
|
-
`verify_then_repreview`):
|
|
214
|
-
|
|
215
|
-
a
|
|
216
|
-
|
|
198
|
+
`verify_then_repreview`): an identical plan is never sent again, even when previewed under a new
|
|
199
|
+
handle. Re-read the target and verify in Autotask. A changed payload or changed stable vendor facts
|
|
200
|
+
creates a different plan; it still waits for the 10-minute target cooldown after an unknown outcome.
|
|
201
|
+
Payload field names are matched case-insensitively, and keys that
|
|
217
202
|
differ only by case, or are not plain ASCII field names, are refused. `STATE_UNAVAILABLE` means the
|
|
218
203
|
state dir could not be checked (`retry`) or the server's own registration in the state dir was removed (`stop`; see below). Use a separate Autotask API user for operator deployments, and run
|
|
219
204
|
the live write leg on throwaway data before relying on it (B7 in the 0.6.0 plan).
|
|
220
205
|
|
|
206
|
+
Upgrade note (#250): stop **all** processes sharing a state directory before upgrading and restart
|
|
207
|
+
all on the same version. Mixed old/new versions are unsupported. Old pending plans must be previewed
|
|
208
|
+
again. Existing ledger entries and historical chained audit lines remain on disk; new audit lines are
|
|
209
|
+
plain JSONL. Old move snapshot files are inert and are not automatically deleted.
|
|
210
|
+
|
|
221
211
|
#### The state lock: recovery
|
|
222
212
|
|
|
223
213
|
Many processes may share a state dir (issue #68): every process registers its boot in a holder
|
|
@@ -243,7 +233,7 @@ The succession rules:
|
|
|
243
233
|
lease is expired 15 s after the old container stopped, so the next boot takes the lock over on
|
|
244
234
|
its own. With a new hostname (the docker default: the container id) the R6 host refusal applies
|
|
245
235
|
and the operator deletes `<state dir>/lock` (the case below). Every takeover is appended to the
|
|
246
|
-
`audit.jsonl`
|
|
236
|
+
`audit.jsonl` log as a `lock_takeover` line, and `/ready` reports the lock's age, holder
|
|
247
237
|
id, lease freshness and the holder count, so an orchestrator can tell a stale lock from a live
|
|
248
238
|
one.
|
|
249
239
|
|
|
@@ -285,6 +275,8 @@ signal. Keep the rest of the state dir; `audit.jsonl` is the write history.
|
|
|
285
275
|
|
|
286
276
|
### Query results: TSV, `output_path` and the saved-file shape
|
|
287
277
|
|
|
278
|
+
`autotask_query` always wraps untrusted text and uses the SDK's default transport and labels; `via`, `names`, and `untrusted_wrapper` are refused as unknown inputs.
|
|
279
|
+
|
|
288
280
|
`autotask_query` answers `data.result` with the SDK's bounded-query envelope:
|
|
289
281
|
|
|
290
282
|
```
|
|
@@ -301,12 +293,11 @@ writes a result file on its own: when a result is too large, the MCP host persis
|
|
|
301
293
|
tool result, and the file it writes holds this same envelope as JSON — so a saved result can be read
|
|
302
294
|
back with exactly the fields above.
|
|
303
295
|
|
|
304
|
-
|
|
296
|
+
Two per-call options change the result form:
|
|
305
297
|
|
|
306
298
|
| Option | Effect |
|
|
307
299
|
|--------|--------|
|
|
308
300
|
| `format: 'tsv'` | serves the table as `data.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. |
|
|
309
|
-
| `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. |
|
|
310
301
|
| `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. |
|
|
311
302
|
|
|
312
303
|
`output_path` is deliberately strict: the deployment sets an **absolute** `AUTOTASK_OUTPUT_DIR`
|
|
@@ -335,7 +326,7 @@ so `O_EXCL` refuses it too.
|
|
|
335
326
|
|
|
336
327
|
The walk stops at 10,000 rows with an explicit truncation note in the summary (`hasMore: true` then
|
|
337
328
|
means the cap, not only the query limit). The call is refused outright on the `http` transport (the
|
|
338
|
-
caller is a request, not the process owner), and it cannot be combined with `
|
|
329
|
+
caller is a request, not the process owner), and it cannot be combined with `latest` or
|
|
339
330
|
`note_chars` — one result form per call.
|
|
340
331
|
|
|
341
332
|
The invoice export is the second `output_path` file kind (issue #54): `autotask_invoice_get` with
|
|
@@ -461,10 +452,9 @@ non-zero on an invalid config (fail-fast) instead of failing at the first tool c
|
|
|
461
452
|
|----------|----------|---------|---------|
|
|
462
453
|
| `MCP_TRANSPORT` | no | `http` | `stdio` or `http`; the CLI `--transport` flag overrides |
|
|
463
454
|
| `AUTOTASK_PROFILE` | no | `readonly` | `readonly` or `operator` (see [Profiles and writes](#profiles-and-writes)) |
|
|
464
|
-
| `AUTOTASK_WRITE` | operator | — | per-operation `allow`/`
|
|
455
|
+
| `AUTOTASK_WRITE` | operator | — | per-operation `allow`/`deny`; hard rules require human confirmation (see [docs/write-gate.md](docs/write-gate.md)) |
|
|
465
456
|
| `AUTOTASK_DEDUP` | no | — | create duplicate-check fields, e.g. `companies.create:companyName` |
|
|
466
|
-
| `
|
|
467
|
-
| `AUTOTASK_TRUSTED_HOST_PROMPT` | no | — | `stdio` and client ids whose own prompt confirms a gated apply |
|
|
457
|
+
| `AUTOTASK_EXPENSE_REPORT_SUBMITTER_PIN` | no | — | one resource id admitted for a company-less `expense_reports.create` (see [write-gate.md](docs/write-gate.md)) |
|
|
468
458
|
| `AUTOTASK_STATE_DIR` | operator | — | the operator state dir (0700) |
|
|
469
459
|
| `AUTOTASK_RUN_SCOPE` | operator on stdio or gateway | — | `{"companyIds":[...]}` or `{"all":true}` |
|
|
470
460
|
| `AUTOTASK_PRINCIPAL` | no | `stdio` | the stdio operator principal |
|
|
@@ -478,10 +468,9 @@ non-zero on an invalid config (fail-fast) instead of failing at the first tool c
|
|
|
478
468
|
| `AUTOTASK_BASE_URL` | stdio: yes · http: no (default origin) | — | the zone origin, e.g. `https://webservices6.autotask.net` (zone lives in the host; base path `/atservicesrest` is fixed — never configurable) |
|
|
479
469
|
| `AUTOTASK_ALLOWED_BASE_HOSTS` | http: unless `AUTOTASK_ALLOW_ANY_BASE_HOST=true` | — | comma-separated exact hostnames or `*.suffix` subdomain suffixes a caller may name in `X-Autotask-Base-Url` (no scheme/port/path). Unset + default origin set → only that origin accepted. Unset + no default → boot refused unless the lab-only flag. **Recommended production value: `*.autotask.net`**. Literal private-IP origins are always refused. |
|
|
480
470
|
| `AUTOTASK_ALLOW_ANY_BASE_HOST` | no | `false` | lab-only opt-in to boot with neither allow-list nor default origin; `warn`-logged at boot (decision D-4); refused with the operator profile. Not set in production. |
|
|
481
|
-
| `
|
|
471
|
+
| `AUTOTASK_CACHE` | no | `on` | the single off switch for the SDK's per-tenant in-process caches (see [Per-tenant pool & cache](#per-tenant-pool--cache)): `on` (default) sends no cache block — the SDK's default-on caches apply to each pooled tenant client (read ~5 min + the reference tables at 1 h, field metadata ~5 min); `off` sends `cache: false` (every read re-dials the vendor) |
|
|
482
472
|
| `AUTOTASK_CLOSED_TICKET_STATUS_IDS` | no | unset | comma-separated ticket status ids that mean closed (e.g. `5,9`), pinning what the open-ticket parts of `autotask_resource_workload`, `autotask_contact_full_profile` and `autotask_company_full_profile` exclude; unset = read from the tenant's live status picklist (the ids used are reported in the part's `closedStatus`); if the picklist can't be read and nothing is pinned, `autotask_resource_workload` and `autotask_contact_full_profile` degrade the open-ticket part to `absent: CLOSED_STATUS_UNRESOLVED` rather than answer on a guessed id, while `autotask_company_full_profile` (which keeps the SDK's `allowAssumed: true`) answers on the SDK's assumed closed id and says so in the part's disclosure note |
|
|
483
473
|
| `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) |
|
|
484
|
-
| `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 |
|
|
485
474
|
| `AUTOTASK_OUTPUT_DIR` | no | unset | the ONE directory the `output_path` option may write into (the bounded query tools and the invoice export; stdio transport only). Unset = defaults to `output/` under `AUTOTASK_STATE_DIR` (the feature refuses when neither is set); 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) |
|
|
486
475
|
| `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) |
|
|
487
476
|
| `AUTOTASK_MAX_CONCURRENT` | no | 3 (agent profile) | override the SDK's local concurrency bound |
|
|
@@ -641,17 +630,16 @@ security decision D-1 ratified): a bounded **in-process** client pool with a per
|
|
|
641
630
|
cache, because a fresh client per request would silently disable the SDK's local vendor-budget
|
|
642
631
|
guards (rate limiter, 429 cooldown) — a worse security posture, not a safer one.
|
|
643
632
|
|
|
644
|
-
- **Pool** — `createClientPool`
|
|
645
|
-
|
|
646
|
-
(user, integration code, secret, resolved base URL,
|
|
647
|
-
|
|
648
|
-
deletes the entry (no negative caching).
|
|
649
|
-
- **Cache** — `
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
(every read re-dials the vendor).
|
|
633
|
+
- **Pool** — the SDK's `createClientPool` (no server-side wrapper; issue #252): a bounded LRU of
|
|
634
|
+
tenant clients at the SDK default `maxSize` 128. Key = SHA-256 digest of the
|
|
635
|
+
**normalised credential tuple** (user, integration code, secret, resolved base URL,
|
|
636
|
+
impersonation id). Eviction is logged without values. A vendor `401` (or an
|
|
637
|
+
impersonation-scope `403`) deletes the entry (no negative caching).
|
|
638
|
+
- **Cache** — `AUTOTASK_CACHE=on` (default) sends no cache block — the SDK's default-on caches
|
|
639
|
+
apply to each pooled tenant client (SDK 0.9.0's default-on change: read ~5 min, the reference
|
|
640
|
+
tables ~1 h, field metadata ~5 min). Repeated reads of the same record are served from that
|
|
641
|
+
tenant's cache until TTL expiry; composites de-duplicate their 3–8 fan-out reads through it.
|
|
642
|
+
`off` sends `cache: false` (every read re-dials the vendor).
|
|
655
643
|
- **Boundaries — never relaxed:** in-process only (never on disk), **per-tenant** (one tenant's
|
|
656
644
|
cached records or limiter state can never answer another tenant's request), TTL-bounded, cleared
|
|
657
645
|
on eviction and on `401`. The same boundary covers the limiter/cooldown state.
|
|
@@ -710,12 +698,11 @@ guards (rate limiter, 429 cooldown) — a worse security posture, not a safer on
|
|
|
710
698
|
liveness-only healthcheck (no vendor call), direct `SIGTERM` (stop accepting, 2 s bounded drain of
|
|
711
699
|
in-flight work, exit 0), TLS termination at the gateway on a private unpublished network.
|
|
712
700
|
- **Supply chain:** `node-autotask` is the only **vendor** runtime dependency (the other two are
|
|
713
|
-
`@modelcontextprotocol/server` and `zod`); the pre-publish checklist
|
|
714
|
-
|
|
715
|
-
lifecycle hooks**, asserts the resolved `@modelcontextprotocol/server` version is ≥ 2.3.0 (the
|
|
701
|
+
`@modelcontextprotocol/server` and `zod`); the pre-publish checklist asserts it carries **zero
|
|
702
|
+
runtime dependencies** and **no install lifecycle hooks**, asserts the resolved `@modelcontextprotocol/server` version is ≥ 2.3.0 (the
|
|
716
703
|
baseline that serves the era pair on one endpoint), runs a production dependency audit, and
|
|
717
|
-
scans the image.
|
|
718
|
-
|
|
704
|
+
scans the image. CI (`.github/workflows/ci.yml`) runs the full gate on every PR; npm releases are
|
|
705
|
+
published only by the tag-triggered release workflow (see [docs/RELEASING.md](docs/RELEASING.md)).
|
|
719
706
|
|
|
720
707
|
Full control set: [`docs/architecture.md`](docs/architecture.md) (sections 7.3, 8 and 10).
|
|
721
708
|
|
|
@@ -737,10 +724,9 @@ mirrored-surface re-review, gates), not a range float: see the
|
|
|
737
724
|
|
|
738
725
|
## Development
|
|
739
726
|
|
|
740
|
-
- Node **24** (current LTS; the SDK requires `node >= 24`), **pnpm 10**.
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
is a pre-publish requirement; gates run manually — no CI in this repo).
|
|
727
|
+
- Node **24** (current LTS; the SDK requires `node >= 24`), **pnpm 10**. `pnpm-lock.yaml` is committed
|
|
728
|
+
and installs are frozen (`pnpm install --frozen-lockfile`). `pnpm verify` is the full gate; CI runs it on
|
|
729
|
+
every PR and push to `main`.
|
|
744
730
|
- `pnpm i` — install · `pnpm build` — compile to `dist/` · `pnpm typecheck` · `pnpm lint`
|
|
745
731
|
- Tests: `pnpm test` (all) or per suite: `pnpm exec vitest run test/unit` / `test/integration` /
|
|
746
732
|
`test/conformance`. **No live vendor API and no credentials are ever required** — every suite
|
package/capabilities.json
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"description": "Machine-readable capability catalogue for iit-mcp-autotask: the tools the server can serve, with the toolset and the lowest profile that serves each. Everything else in the registry is reachable through the generic layer (autotask_catalog / autotask_describe / autotask_read) and documented in docs/capability-matrix.md. Generated from src/tools/surface.ts by scripts/build-capabilities.mjs.",
|
|
5
5
|
"exposedToolCount": {
|
|
6
6
|
"readonlyCore": 10,
|
|
7
|
-
"readonlyAll":
|
|
7
|
+
"readonlyAll": 18,
|
|
8
8
|
"operatorCore": 12,
|
|
9
|
-
"operatorAll":
|
|
9
|
+
"operatorAll": 20
|
|
10
10
|
},
|
|
11
11
|
"profiles": {
|
|
12
12
|
"readonly": {
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
],
|
|
41
41
|
"finance": [
|
|
42
42
|
"autotask_quote_get",
|
|
43
|
-
"autotask_invoice_get"
|
|
43
|
+
"autotask_invoice_get",
|
|
44
|
+
"autotask_revenue_rollup"
|
|
44
45
|
],
|
|
45
46
|
"operator": [
|
|
46
47
|
"autotask_write",
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
},
|
|
68
69
|
{
|
|
69
70
|
"id": "autotask.platform.catalog",
|
|
70
|
-
"description": "Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,
|
|
71
|
+
"description": "Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,280 registry operations, with effect and whether this deployment serves it. No vendor call.",
|
|
71
72
|
"mcpPrimitive": "tool",
|
|
72
73
|
"toolName": "autotask_catalog",
|
|
73
74
|
"type": "read",
|
|
@@ -99,7 +100,7 @@
|
|
|
99
100
|
},
|
|
100
101
|
{
|
|
101
102
|
"id": "autotask.generic.read",
|
|
102
|
-
"description": "Run
|
|
103
|
+
"description": "Run one or several reads in one call by exact registry key: unbilled work, profitability and margin, SLA health. Scope by company/contract ids (unscoped = tenant walk); totals: autotask_query group_by; slim: includeFields, noteChars.",
|
|
103
104
|
"mcpPrimitive": "tool",
|
|
104
105
|
"toolName": "autotask_read",
|
|
105
106
|
"type": "read",
|
|
@@ -179,7 +180,7 @@
|
|
|
179
180
|
},
|
|
180
181
|
{
|
|
181
182
|
"id": "autotask.company.full_profile",
|
|
182
|
-
"description": "One company with locations, contacts, notes, open tickets and projects
|
|
183
|
+
"description": "One company with locations, contacts, notes, open tickets and projects (capped: true = more exist; raise the limit). fields {part:[names]} trims columns. Open = not tenant-closed. Missing parts degrade to typed absent.",
|
|
183
184
|
"mcpPrimitive": "tool",
|
|
184
185
|
"toolName": "autotask_company_full_profile",
|
|
185
186
|
"type": "read",
|
|
@@ -195,7 +196,7 @@
|
|
|
195
196
|
},
|
|
196
197
|
{
|
|
197
198
|
"id": "autotask.resource.workload",
|
|
198
|
-
"description": "A resource's open tickets (primary or secondary, de-duplicated), assignments, time off. For \"how many\" use totals (exact; capped = link cap hit, not row capped; extra reads
|
|
199
|
+
"description": "A resource's open tickets (primary or secondary, de-duplicated), assignments, time off. For \"how many\" use totals (exact; capped = link cap hit, not row capped; extra reads; totalsAbsent = failed). Missing parts: typed absent.",
|
|
199
200
|
"mcpPrimitive": "tool",
|
|
200
201
|
"toolName": "autotask_resource_workload",
|
|
201
202
|
"type": "read",
|
|
@@ -243,7 +244,7 @@
|
|
|
243
244
|
},
|
|
244
245
|
{
|
|
245
246
|
"id": "autotask.contact.full_profile",
|
|
246
|
-
"description": "One contact with their company and open tickets (closed set from the tenant status picklist
|
|
247
|
+
"description": "One contact with their company and open tickets (closed set from the tenant status picklist). Contains personal data. Missing parts degrade to typed absent.",
|
|
247
248
|
"mcpPrimitive": "tool",
|
|
248
249
|
"toolName": "autotask_contact_full_profile",
|
|
249
250
|
"type": "read",
|
|
@@ -275,7 +276,7 @@
|
|
|
275
276
|
},
|
|
276
277
|
{
|
|
277
278
|
"id": "autotask.contract.full_profile",
|
|
278
|
-
"description": "One contract with services, billing rules,
|
|
279
|
+
"description": "One contract with services, billing rules, newest charges and service units. Monthly P&L: autotask_read (guide: contract-profitability).",
|
|
279
280
|
"mcpPrimitive": "tool",
|
|
280
281
|
"toolName": "autotask_contract_full_profile",
|
|
281
282
|
"type": "read",
|
|
@@ -321,6 +322,22 @@
|
|
|
321
322
|
"openWorld": true
|
|
322
323
|
}
|
|
323
324
|
},
|
|
325
|
+
{
|
|
326
|
+
"id": "autotask.finance.revenue_rollup",
|
|
327
|
+
"description": "Revenue for recurring and fixed price contracts by vertical, company or month, with excluded amounts and coverage. One scope required; full-month windows in the user zone. May take 15-45 seconds. Finance playbook: revenue-rollup.",
|
|
328
|
+
"mcpPrimitive": "tool",
|
|
329
|
+
"toolName": "autotask_revenue_rollup",
|
|
330
|
+
"type": "read",
|
|
331
|
+
"toolset": "finance",
|
|
332
|
+
"profile": "readonly",
|
|
333
|
+
"backingOperation": "operations.revenueRollup",
|
|
334
|
+
"annotations": {
|
|
335
|
+
"readOnly": true,
|
|
336
|
+
"idempotent": true,
|
|
337
|
+
"destructive": false,
|
|
338
|
+
"openWorld": true
|
|
339
|
+
}
|
|
340
|
+
},
|
|
324
341
|
{
|
|
325
342
|
"id": "autotask.generic.write",
|
|
326
343
|
"description": "Preview any write (create, update, delete or a composite helper) by exact key, args in declaration order, ids never names: validates, reads the target, returns a plan and diff. A composite op echoes its args only, no field diff.",
|
|
@@ -361,7 +378,7 @@
|
|
|
361
378
|
"autotask_describe",
|
|
362
379
|
"autotask_read"
|
|
363
380
|
],
|
|
364
|
-
"backingRegistryOperations":
|
|
381
|
+
"backingRegistryOperations": 1280,
|
|
365
382
|
"note": "autotask_catalog finds the exact operation key (ranked search), autotask_describe serves its contract, autotask_read runs any effect=read key. Writes go through the operator profile (autotask_write then autotask_apply); authenticate.* is never served."
|
|
366
383
|
},
|
|
367
384
|
"schemas": {}
|
package/dist/access/gate.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ClientCapabilities, type InputRequiredResult } from '@modelcontextprotocol/server';
|
|
2
2
|
import { type ToolReturn } from '../tools/results/envelope.js';
|
|
3
|
-
import type { Confirmation } from '../tools/write/apply
|
|
3
|
+
import type { Confirmation } from '../tools/write/apply.js';
|
|
4
4
|
import type { PlanRecord } from '../write/store.js';
|
|
5
5
|
/** The key the confirm form's answer rides under in `inputResponses`. */
|
|
6
6
|
export declare const GATE_KEY = "confirm";
|
|
@@ -23,7 +23,6 @@ export declare function formElicitation(extra: unknown, caller: Caller, legacyCa
|
|
|
23
23
|
export declare function gateApply(a: {
|
|
24
24
|
plan: PlanRecord;
|
|
25
25
|
caller: Caller;
|
|
26
|
-
trusted: ReadonlySet<string>;
|
|
27
26
|
extra: unknown;
|
|
28
27
|
legacyCaps: ClientCapabilities | undefined;
|
|
29
28
|
}): GateOutcome;
|
package/dist/access/gate.js
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* access/gate.ts — the apply-time human gate for a gated plan (#178).
|
|
3
|
-
*
|
|
4
|
-
* A trusted caller's own host prompt counts as the confirmation (AUTOTASK_TRUSTED_HOST_PROMPT). Any
|
|
5
|
-
* other caller whose client shows form elicitation gets one plain confirm form through the SDK's
|
|
6
|
-
* multi-round-trip `input_required` result, and the client retries the same call with the answer.
|
|
7
|
-
* The plan is already held on the server, so there is no code, key or `requestState` to carry.
|
|
8
|
-
* Everything else is a typed `GATE_UNSUPPORTED` deny: an untrusted 2025-era client over HTTP
|
|
9
|
-
* included, because the legacy shim cannot push a request from a stateless per-request instance.
|
|
10
|
-
*/
|
|
1
|
+
/** Apply-time human confirmation through MCP form elicitation; unsupported clients fail closed. */
|
|
11
2
|
import * as z from 'zod';
|
|
12
3
|
import { inputRequired } from '@modelcontextprotocol/server';
|
|
13
4
|
import { cleanUntrustedText } from '../tools/results/untrusted.js';
|
|
14
5
|
import {} from '../tools/results/envelope.js';
|
|
15
6
|
import { answer, reason } from '../tools/write/common.js';
|
|
16
|
-
import { approvalDigest } from '../write/canonical.js';
|
|
17
7
|
/** The key the confirm form's answer rides under in `inputResponses`. */
|
|
18
8
|
export const GATE_KEY = 'confirm';
|
|
19
9
|
const DATA_CAP = 500;
|
|
@@ -53,24 +43,20 @@ export function formElicitation(extra, caller, legacyCaps) {
|
|
|
53
43
|
}
|
|
54
44
|
/** Decide one gated plan's apply: a confirmation, the confirm form, or a typed deny. Zero vendor calls on every path. */
|
|
55
45
|
export function gateApply(a) {
|
|
56
|
-
const { plan, caller,
|
|
57
|
-
const digest = approvalDigest(plan.planHash, plan.facts);
|
|
46
|
+
const { plan, caller, extra, legacyCaps } = a;
|
|
58
47
|
const deny = (code, detail) => ({
|
|
59
48
|
result: answer('denied', 'stop', [reason(code, detail)], { operation: plan.op, targets: plan.targets, plan: plan.handle }, { plan: plan.handle, planHash: plan.planHash }),
|
|
60
49
|
});
|
|
61
|
-
if ((caller.transport === 'stdio' && trusted.has('stdio')) || (caller.transport === 'http' && caller.clientId !== undefined && trusted.has(caller.clientId))) {
|
|
62
|
-
return { ok: { via: 'host_prompt', digest } };
|
|
63
|
-
}
|
|
64
50
|
// an answer counts only from a client that declared form elicitation; it is untrusted input, so only an explicit accept confirms
|
|
65
51
|
if (formElicitation(extra, caller, legacyCaps)) {
|
|
66
52
|
const response = extra?.mcpReq?.inputResponses?.[GATE_KEY];
|
|
67
53
|
if (response !== undefined) {
|
|
68
54
|
if (response?.action === 'accept')
|
|
69
|
-
return { ok: { via: 'elicitation',
|
|
55
|
+
return { ok: { via: 'elicitation', planHash: plan.planHash } };
|
|
70
56
|
return deny('GATE_DECLINED', `the person declined or dismissed ${plan.op}; nothing was sent`);
|
|
71
57
|
}
|
|
72
58
|
return { result: inputRequired({ inputRequests: { [GATE_KEY]: inputRequired.elicit({ message: gateMessage(plan), requestedSchema: z.object({}) }) } }) };
|
|
73
59
|
}
|
|
74
|
-
return deny('GATE_UNSUPPORTED', `${plan.op} needs a person to confirm it, and this client cannot show a confirm form
|
|
60
|
+
return deny('GATE_UNSUPPORTED', `${plan.op} needs a person to confirm it, and this client cannot show a confirm form; nothing was sent`);
|
|
75
61
|
}
|
|
76
62
|
//# sourceMappingURL=gate.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type WriteShape } from '../write/hard-rules.js';
|
|
2
|
-
export type WriteMode = 'allow' | '
|
|
2
|
+
export type WriteMode = 'allow' | 'deny';
|
|
3
3
|
export interface ResolvedOp {
|
|
4
4
|
mode: WriteMode;
|
|
5
5
|
rule: string;
|
|
@@ -7,21 +7,16 @@ export interface ResolvedOp {
|
|
|
7
7
|
upgraded?: true;
|
|
8
8
|
dedup?: readonly string[];
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
perHour?: number;
|
|
12
|
-
perDay?: number;
|
|
13
|
-
}
|
|
10
|
+
/** `expensePin`: the one submitter id a company-less `expense_reports.create` is admitted for (absent = no exception). */
|
|
14
11
|
export interface LoadedPolicy {
|
|
15
12
|
table: ReadonlyMap<string, ResolvedOp>;
|
|
16
|
-
|
|
13
|
+
expensePin?: number;
|
|
17
14
|
policyHash: string;
|
|
18
15
|
}
|
|
19
16
|
export declare function resolveWritePolicy(env: {
|
|
20
17
|
write: string;
|
|
21
18
|
dedup?: string;
|
|
22
|
-
|
|
19
|
+
expensePin?: string;
|
|
23
20
|
}): LoadedPolicy;
|
|
24
|
-
/**
|
|
25
|
-
export declare function parseTrustedHosts(raw: string | undefined): ReadonlySet<string>;
|
|
26
|
-
/** A `gate` row, or a non-deny move row (a move can escalate to a gate). */
|
|
21
|
+
/** Any permitted write whose hard rule requires a human, or a move that can escalate. */
|
|
27
22
|
export declare const anyGate: (loaded: LoadedPolicy) => boolean;
|