iit-mcp-autotask 0.0.0-stage → 0.2.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 +80 -0
- package/LICENSE +21 -0
- package/README.md +495 -2
- package/capabilities.json +603 -0
- package/dist/auth.d.ts +61 -0
- package/dist/auth.js +110 -0
- package/dist/base-hosts.d.ts +38 -0
- package/dist/base-hosts.js +70 -0
- package/dist/config.d.ts +136 -0
- package/dist/config.js +351 -0
- package/dist/credential.d.ts +69 -0
- package/dist/credential.js +125 -0
- package/dist/doctor.d.ts +82 -0
- package/dist/doctor.js +158 -0
- package/dist/http.d.ts +47 -0
- package/dist/http.js +240 -0
- package/dist/lifecycle.d.ts +29 -0
- package/dist/lifecycle.js +52 -0
- package/dist/logger.d.ts +26 -0
- package/dist/logger.js +28 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +198 -0
- package/dist/mocks/fixture-ops.d.ts +79 -0
- package/dist/mocks/fixture-ops.js +65 -0
- package/dist/mocks/fixtures.d.ts +100 -0
- package/dist/mocks/fixtures.js +94 -0
- package/dist/mocks/harness.d.ts +89 -0
- package/dist/mocks/harness.js +195 -0
- package/dist/mocks/wire.d.ts +74 -0
- package/dist/mocks/wire.js +183 -0
- package/dist/policy.d.ts +77 -0
- package/dist/policy.js +133 -0
- package/dist/stdio.d.ts +20 -0
- package/dist/stdio.js +28 -0
- package/dist/tools/catalog-data.d.ts +159 -0
- package/dist/tools/catalog-data.js +377 -0
- package/dist/tools/composites.d.ts +13 -0
- package/dist/tools/composites.js +422 -0
- package/dist/tools/context.d.ts +163 -0
- package/dist/tools/context.js +360 -0
- package/dist/tools/dispatch.d.ts +36 -0
- package/dist/tools/dispatch.js +215 -0
- package/dist/tools/lists.d.ts +31 -0
- package/dist/tools/lists.js +125 -0
- package/dist/tools/meta.d.ts +22 -0
- package/dist/tools/meta.js +207 -0
- package/dist/tools/register.d.ts +31 -0
- package/dist/tools/register.js +39 -0
- package/dist/tools/resolves.d.ts +16 -0
- package/dist/tools/resolves.js +63 -0
- package/dist/tools/results.d.ts +79 -0
- package/dist/tools/results.js +455 -0
- package/dist/tools/schemas.d.ts +286 -0
- package/dist/tools/schemas.js +367 -0
- package/dist/tools/server.d.ts +56 -0
- package/dist/tools/server.js +86 -0
- package/dist/tools.d.ts +7 -0
- package/dist/tools.js +7 -0
- package/package.json +62 -4
- package/server.json +35 -0
package/.env.example
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# The credentials load recipe (issue #6) — read this before your first live session.
|
|
2
|
+
#
|
|
3
|
+
# A .env value may be SINGLE-QUOTED on purpose: a secret containing `$` must be, or the shell would
|
|
4
|
+
# expand it. Those quotes are part of the shell syntax, NOT part of the credential:
|
|
5
|
+
#
|
|
6
|
+
# set -a; . ./.env; set +a # shell sourcing — the ONLY load recipe this server documents
|
|
7
|
+
# autotask-mcp doctor # confirm the shape before any live session (zero vendor calls)
|
|
8
|
+
#
|
|
9
|
+
# Do NOT load this file with a quote-preserving dotenv parser (most `dotenv` variants, a hand-rolled
|
|
10
|
+
# split('=')): it hands the value over with its quotes intact, and the vendor answers 401 "Bad
|
|
11
|
+
# credentials" — which reads like a rotated credential, on an account where repeated auth failures
|
|
12
|
+
# risk a lock. The server refuses such a value before dialing (CREDENTIAL_MALFORMED) and `doctor`
|
|
13
|
+
# reports it as `shapeOk: false`, but the fix is the load recipe.
|
|
14
|
+
#
|
|
15
|
+
# A green `doctor` makes any later 401 vendor-side (rotated, revoked or role-restricted credential).
|
|
16
|
+
# See README §Credentials & env files.
|
|
17
|
+
|
|
18
|
+
# iit-mcp-autotask — non-secret configuration only.
|
|
19
|
+
# Real credentials never live in a committed file: under stdio the process environment IS the
|
|
20
|
+
# trust boundary (set them in the launching environment); under http they travel per request
|
|
21
|
+
# in the X-Autotask-* headers injected by the gateway.
|
|
22
|
+
|
|
23
|
+
# stdio (local: Claude Code / Claude Desktop / VS Code) or http (remote MCP clients / gateway).
|
|
24
|
+
MCP_TRANSPORT=stdio
|
|
25
|
+
|
|
26
|
+
# --- Credentials (stdio: required from the environment; http: optional discouraged defaults) ---
|
|
27
|
+
# stdio: all three are REQUIRED for a live server; without them the server still boots and serves
|
|
28
|
+
# the full surface in credential-less mode (initialize/tools/list work; tool calls refuse).
|
|
29
|
+
# http: when set, every request WITHOUT its own X-Autotask-* headers runs as this identity and the
|
|
30
|
+
# identity is only ever presented to the AUTOTASK_BASE_URL default origin. Left commented so a
|
|
31
|
+
# copied example does not set empty values.
|
|
32
|
+
# AUTOTASK_API_INTEGRATION_CODE=
|
|
33
|
+
# AUTOTASK_USERNAME=
|
|
34
|
+
# AUTOTASK_SECRET=
|
|
35
|
+
# AUTOTASK_IMPERSONATION_RESOURCE_ID=
|
|
36
|
+
|
|
37
|
+
# The Autotask origin the server talks to. stdio: REQUIRED (the zone lives in the host, e.g.
|
|
38
|
+
# https://webservices6.autotask.net; base path /atservicesrest is fixed). http: optional DEFAULT
|
|
39
|
+
# origin for requests that send no X-Autotask-Base-Url header.
|
|
40
|
+
# AUTOTASK_BASE_URL=https://webservices6.autotask.net
|
|
41
|
+
|
|
42
|
+
# http only. Hosts a caller may name in X-Autotask-Base-Url: comma-separated exact hostnames or
|
|
43
|
+
# *.suffix subdomain suffixes (no scheme/port/path). Unset alongside a set default origin -> only
|
|
44
|
+
# that origin is accepted. Unset with no default origin -> the server REFUSES to boot unless
|
|
45
|
+
# AUTOTASK_ALLOW_ANY_BASE_HOST=true. Literal private-IP origins are always refused.
|
|
46
|
+
# Recommended production value: the line below.
|
|
47
|
+
# AUTOTASK_ALLOWED_BASE_HOSTS=*.autotask.net
|
|
48
|
+
# AUTOTASK_ALLOW_ANY_BASE_HOST=false
|
|
49
|
+
|
|
50
|
+
# --- Mutation surface (opt-in; reads are never affected) ---
|
|
51
|
+
# Omit all mutation tools entirely. Set false before enabling a write policy.
|
|
52
|
+
AUTOTASK_READ_ONLY=true
|
|
53
|
+
# deny (default — forgetting this yields a read-only server, never an open one) | allow_list | all
|
|
54
|
+
# (must be typed out deliberately).
|
|
55
|
+
AUTOTASK_WRITE_POLICY=deny
|
|
56
|
+
# Only under allow_list: comma-separated registry operation keys (e.g. time_entries.create).
|
|
57
|
+
# Must be non-empty and every key must exist in the SDK registry and be a write, or boot fails
|
|
58
|
+
# with the variable named. Ignored under deny/all.
|
|
59
|
+
# AUTOTASK_WRITE_ALLOW=time_entries.create
|
|
60
|
+
|
|
61
|
+
# --- Secret reads (independent of the write policy) ---
|
|
62
|
+
# deny (default) refuses authenticate.get, which returns a plaintext vendor token that the SDK's
|
|
63
|
+
# redaction deliberately exempts. allow permits it; the token is still never logged.
|
|
64
|
+
AUTOTASK_SECRET_READS=deny
|
|
65
|
+
|
|
66
|
+
# --- Per-tenant read cache (in-process; never cross-tenant; never on disk) ---
|
|
67
|
+
# on (default): each pooled tenant client applies the SDK reference TTL table (structural ~1 h /
|
|
68
|
+
# mutable ~5 min / base ~1 min) plus the field-metadata cache; repeated reads of the same record
|
|
69
|
+
# are served from the tenant's cache until TTL expiry. off: every read re-dials the vendor.
|
|
70
|
+
AUTOTASK_CACHE_PRESET=on
|
|
71
|
+
|
|
72
|
+
# --- Vendor budget (the server boots the SDK in the conservative `agent` profile; overrides here) ---
|
|
73
|
+
# AUTOTASK_RATE_LIMIT_PER_HOUR=8000
|
|
74
|
+
# AUTOTASK_MAX_CONCURRENT=3
|
|
75
|
+
# AUTOTASK_TIMEOUT_MS=20000
|
|
76
|
+
# AUTOTASK_MAX_RETRIES=1
|
|
77
|
+
|
|
78
|
+
# --- Operational ---
|
|
79
|
+
PORT=8080
|
|
80
|
+
LOG_LEVEL=info
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Max Soukhomlinov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,496 @@
|
|
|
1
|
-
#
|
|
1
|
+
# iit-mcp-autotask
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server for the Autotask (Datto) PSA domain, built on the [`node-autotask`](https://www.npmjs.com/package/node-autotask) SDK.
|
|
4
|
+
|
|
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
|
|
7
|
+
context into one call, a generic capability layer for the long tail, bounded responses, predictable
|
|
8
|
+
schemas, strong security boundaries, and predictable per-tenant in-process state (pooled client +
|
|
9
|
+
TTL read cache).
|
|
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
|
|
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
|
|
15
|
+
> to a local checkout.
|
|
16
|
+
|
|
17
|
+
## Tool surface
|
|
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
|
|
21
|
+
registry stays reachable through the **generic capability layer** — never pre-registered.
|
|
22
|
+
|
|
23
|
+
Composites bound **each part independently**: every part reads its own declared limit argument
|
|
24
|
+
(e.g. `ticket_limit`, `task_limit`, `time_off_limit`, `service_limit`, `billing_rule_limit`,
|
|
25
|
+
`item_limit`, `contact_limit`, `note_limit`, `project_limit`), defaulting to 500 per part — a part
|
|
26
|
+
miss never fails the call (typed `absent`); only a missing root record fails the composite.
|
|
27
|
+
|
|
28
|
+
| Layer | Tools | Role |
|
|
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 |
|
|
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 |
|
|
33
|
+
|
|
34
|
+
Machine-readable catalogue: [`capabilities.json`](capabilities.json) · server metadata: [`server.json`](server.json) ·
|
|
35
|
+
per-workflow mapping: [`docs/capability-matrix.md`](docs/capability-matrix.md) · full design: [`docs/architecture.md`](docs/architecture.md).
|
|
36
|
+
|
|
37
|
+
### The 5-META generic layer
|
|
38
|
+
|
|
39
|
+
The five META tools are the SDK's own live-verified governance layer, adopted verbatim (a recorded
|
|
40
|
+
decision — see `docs/architecture.md` §2.5):
|
|
41
|
+
|
|
42
|
+
- `autotask_catalog` — list registry operations (bounded: limit default 40 / max 500, tier + effect filters).
|
|
43
|
+
- `autotask_describe` — exact input/output schema + examples + error codes for one operation by key.
|
|
44
|
+
- `autotask_read` — route any `effect=read` operation by **exact registry key** (the 363 metadata-trio
|
|
45
|
+
reads with no dedicated tool ride here). Read-only by construction; a write/delete routed here is
|
|
46
|
+
**refused before any request** (the message names the correct tool).
|
|
47
|
+
- `autotask_write` — route any `effect=write` operation, **dry-run-first**: the first call always
|
|
48
|
+
travels `dry_run: true` (pinned `DryRunResult` envelope; the mutating request is never issued);
|
|
49
|
+
`dry_run: false` applies. The 3 approval-gated writes additionally require `confirm === <op key>`
|
|
50
|
+
in both modes.
|
|
51
|
+
- `autotask_delete` — route any `effect=destructive` operation; dry-run-first and `confirm === <op key>`
|
|
52
|
+
**required for all 54 deletes** in both modes.
|
|
53
|
+
|
|
54
|
+
Dispatch accepts only registered exact operation keys — never tool names, never URLs, never HTTP
|
|
55
|
+
paths. Argument sets are closed at the top level: an unknown key is refused pre-request with a
|
|
56
|
+
typed error naming the key and the valid keys. So the ~1,145 non-exposed operations are always
|
|
57
|
+
discoverable (`catalog`/`describe`) and reachable (`read`/`write`/`delete`) — nothing is a silent gap.
|
|
58
|
+
`autotask_catalog` and `autotask_describe` also surface this deployment's policy verdict
|
|
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).
|
|
62
|
+
|
|
63
|
+
### Writes
|
|
64
|
+
|
|
65
|
+
One dedicated write tool exists — `autotask_timeEntry_create` (the confirmed W3 write workflow).
|
|
66
|
+
Everything else that mutates rides `autotask_write` / `autotask_delete` under the SDK's governor
|
|
67
|
+
(dry-run-first, `confirm`, cross-mode refusal) **and** the deployment write policy:
|
|
68
|
+
|
|
69
|
+
| Variable | Default | Effect |
|
|
70
|
+
|----------|---------|--------|
|
|
71
|
+
| `AUTOTASK_READ_ONLY` | `true` | All mutation tools omitted entirely. Set `false` before enabling a write policy. |
|
|
72
|
+
| `AUTOTASK_WRITE_POLICY` | `deny` | `deny` (forgetting it yields a read-only server, never an open one) / `allow_list` / `all` (typed out deliberately). |
|
|
73
|
+
| `AUTOTASK_WRITE_ALLOW` | — | Under `allow_list`: comma-separated registry operation keys (e.g. `time_entries.create`). Boot fails unless non-empty and every key is a write **or destructive** registry operation key (a read key in the list grants nothing). |
|
|
74
|
+
|
|
75
|
+
`AUTOTASK_SECRET_READS=deny` (default) independently refuses `authenticate.get` (a plaintext vendor
|
|
76
|
+
token the SDK's redaction deliberately exempts); the tool stays registered and is refused by policy
|
|
77
|
+
(decision D-3 — stable surface, typed refusal). The policy answers *whether* an operation is
|
|
78
|
+
reachable in this deployment — never *how*: validation, forced dry-run and `confirm` governance
|
|
79
|
+
stay in the SDK.
|
|
80
|
+
|
|
81
|
+
## Transports
|
|
82
|
+
|
|
83
|
+
| Transport | For | Caller auth | Autotask credential |
|
|
84
|
+
|-----------|-----|-------------|---------------------|
|
|
85
|
+
| `http` (Streamable HTTP, `/mcp`) | shared MCP gateway / federation, remote MCP clients | gateway-owned (client auth, rate limiting, tool filtering) | per request: `X-Autotask-*` headers injected by the gateway |
|
|
86
|
+
| `stdio` (`--transport stdio`) | Claude Code, Claude Desktop, VS Code | none — the process boundary is the trust boundary | environment: `AUTOTASK_*` variables |
|
|
87
|
+
|
|
88
|
+
## Deployment
|
|
89
|
+
|
|
90
|
+
### Gateway (http, container)
|
|
91
|
+
|
|
92
|
+
Run behind the shared MCP gateway on a private, unpublished network (the gateway terminates TLS;
|
|
93
|
+
the listener must not be exposed publicly). The gateway owns client authentication, rate limiting,
|
|
94
|
+
per-tenant tool filtering and **credential injection** into the `X-Autotask-*` headers. The server
|
|
95
|
+
is tenant-agnostic: one deployment serves every Autotask tenant; the zone host and identity travel
|
|
96
|
+
per request.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
docker build -t iit-mcp-autotask .
|
|
100
|
+
docker run -d --name iit-mcp-autotask \
|
|
101
|
+
-p 8080:8080 \
|
|
102
|
+
-e AUTOTASK_ALLOWED_BASE_HOSTS=*.autotask.net \
|
|
103
|
+
-e AUTOTASK_BASE_URL=https://webservices6.autotask.net \
|
|
104
|
+
iit-mcp-autotask
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Env-only configuration: no credentials are baked into the image (no `ENV` secrets, no `.env` copy).
|
|
108
|
+
Operational defaults (`MCP_TRANSPORT=http`, `PORT=8080`) are image-level and overridable. `SIGTERM`
|
|
109
|
+
reaches the node process directly (no shell wrapper) and triggers graceful shutdown: stop accepting,
|
|
110
|
+
drain in-flight with a bounded timeout, exit 0. The `HEALTHCHECK` probes `/health` (liveness only —
|
|
111
|
+
it never calls Autotask, so a zone outage cannot cycle the container).
|
|
112
|
+
|
|
113
|
+
### Local (stdio)
|
|
114
|
+
|
|
115
|
+
Needs Node 24+ and your Autotask zone origin. `stdio` is the default transport.
|
|
116
|
+
|
|
117
|
+
**From npm** (once published):
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
claude mcp add autotask \
|
|
121
|
+
--env AUTOTASK_API_INTEGRATION_CODE=... \
|
|
122
|
+
--env AUTOTASK_USERNAME=... \
|
|
123
|
+
--env AUTOTASK_SECRET=... \
|
|
124
|
+
--env AUTOTASK_BASE_URL=https://webservices6.autotask.net \
|
|
125
|
+
-- npx -y iit-mcp-autotask
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
or in `.mcp.json` / Claude Desktop config:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"mcpServers": {
|
|
133
|
+
"autotask": {
|
|
134
|
+
"command": "npx",
|
|
135
|
+
"args": ["-y", "iit-mcp-autotask"],
|
|
136
|
+
"env": {
|
|
137
|
+
"AUTOTASK_API_INTEGRATION_CODE": "...",
|
|
138
|
+
"AUTOTASK_USERNAME": "...",
|
|
139
|
+
"AUTOTASK_SECRET": "...",
|
|
140
|
+
"AUTOTASK_BASE_URL": "https://webservices6.autotask.net"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`npx` fetches the package on first run and caches it; pin with `iit-mcp-autotask@<version>`.
|
|
148
|
+
**From a clone**: `pnpm i && pnpm build`, then point the client at `node /abs/path/iit-mcp-autotask/dist/main.js`.
|
|
149
|
+
|
|
150
|
+
## Endpoints
|
|
151
|
+
|
|
152
|
+
| Endpoint | Purpose | Auth |
|
|
153
|
+
|----------|---------|------|
|
|
154
|
+
| `POST /mcp` | the canonical MCP endpoint (Streamable HTTP transport) | per-request `X-Autotask-*` headers for tool calls |
|
|
155
|
+
| `GET /health` | process alive — pre-auth, **no vendor call** (container healthcheck) | none |
|
|
156
|
+
| `GET /ready` | config valid, SDK plan-hash pin checked, server initialised, client factory ready — **no live vendor call** (credentials are request-scoped) | none |
|
|
157
|
+
| `GET /doctor` | zero-wire credential/config preflight: per credential field `present`/`len`/`prefix2`/`shapeOk` (`AUTOTASK_SECRET`'s prefix is masked to `[REDACTED]` — no character of a secret is ever emitted; identifiers keep their format prefix), the base-url + allow-list resolution (`default_only` / `allow_list` / `default_and_list` / `any`), the SDK plan-hash gate status and the host-policy mode — **no vendor call, by construction** | none |
|
|
158
|
+
| `GET /metadata` | server identity + the MCP protocol **era pair** (baseline `2026-07-28` + legacy fallback `2025-11-25`) + capability-manifest pointer — the served payload is the repo's `server.json` + build stamp | none |
|
|
159
|
+
|
|
160
|
+
## Configuration
|
|
161
|
+
|
|
162
|
+
Copy [.env.example](.env.example). Every variable is Zod-validated at boot; the process exits
|
|
163
|
+
non-zero on an invalid config (fail-fast) instead of failing at the first tool call.
|
|
164
|
+
|
|
165
|
+
| Variable | Required | Default | Meaning |
|
|
166
|
+
|----------|----------|---------|---------|
|
|
167
|
+
| `MCP_TRANSPORT` | no | `stdio` | `stdio` or `http`; the CLI `--transport` flag overrides |
|
|
168
|
+
| `AUTOTASK_API_INTEGRATION_CODE` | stdio: yes · http: no (discouraged default) | — | the `ApiIntegrationCode` credential (stdio env; http: optional discouraged server default, presented only to the default origin) |
|
|
169
|
+
| `AUTOTASK_USERNAME` | stdio: yes · http: no (discouraged default) | — | the `UserName` credential (same semantics) |
|
|
170
|
+
| `AUTOTASK_SECRET` | stdio: yes · http: no (discouraged default) | — | the `Secret` credential (same semantics) |
|
|
171
|
+
| `AUTOTASK_IMPERSONATION_RESOURCE_ID` | no | — | act as another resource in the tenant (`ImpersonationResourceId` header) |
|
|
172
|
+
| `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) |
|
|
173
|
+
| `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. |
|
|
174
|
+
| `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). Not set in production. |
|
|
175
|
+
| `AUTOTASK_READ_ONLY` | no | `true` | omit all mutation tools when true |
|
|
176
|
+
| `AUTOTASK_WRITE_POLICY` | no | `deny` | `deny` / `allow_list` / `all` |
|
|
177
|
+
| `AUTOTASK_WRITE_ALLOW` | only under `allow_list` | — | comma-separated registry operation keys; boot fails on empty/unknown keys |
|
|
178
|
+
| `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) |
|
|
180
|
+
| `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
|
+
| `AUTOTASK_MAX_CONCURRENT` | no | 3 (agent profile) | override the SDK's local concurrency bound |
|
|
182
|
+
| `AUTOTASK_TIMEOUT_MS` | no | 20000 (agent profile) | whole-call budget |
|
|
183
|
+
| `AUTOTASK_MAX_RETRIES` | no | 1 (agent profile) | bounded retries with backoff (idempotent methods, 429/5xx only) |
|
|
184
|
+
| `HOST` | no | `0.0.0.0` | http bind address (`--host` overrides it) |
|
|
185
|
+
| `PORT` | no | `8080` | http listener port (`--port` overrides it) |
|
|
186
|
+
| `LOG_LEVEL` | no | `info` | log level (sensitive-data-safe structured JSON) |
|
|
187
|
+
|
|
188
|
+
**No credentials as CLI flags, ever.** The CLI carries only `--transport` / `--host` / `--port` /
|
|
189
|
+
`--version` / `--help`.
|
|
190
|
+
|
|
191
|
+
## Credentials & env files
|
|
192
|
+
|
|
193
|
+
The 2026-10-07 incident class, in one paragraph: a `.env` value written `AUTOTASK_SECRET='a$b'` was
|
|
194
|
+
fed to the wire **with its quotes intact** by a quote-preserving loader, the vendor answered
|
|
195
|
+
`401 Bad credentials` three times, and the operator read it as a rotated credential — on an account
|
|
196
|
+
where repeated auth failures risk a lock. Nothing in this package inspected the shape of a
|
|
197
|
+
credential before dialing it. Now something does, on every path.
|
|
198
|
+
|
|
199
|
+
### The load recipe (the only supported one)
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
set -a; . ./.env; set +a # shell sourcing: strips the quoting the shell itself added
|
|
203
|
+
autotask-mcp doctor # confirm the shape, zero vendor calls, exit 0 = go
|
|
204
|
+
autotask-mcp # the live session
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- **Single-quote a value that contains `$`.** A secret with `$` in it written bare (`SECRET=a$b`)
|
|
208
|
+
is expanded by the shell to something else entirely. Single quotes stop that — and are then
|
|
209
|
+
removed by the shell on load, which is why the value that reaches the process is the credential.
|
|
210
|
+
- **Do not use a quote-preserving dotenv parser.** Most `dotenv` variants and any hand-rolled
|
|
211
|
+
`Key=Value` split hand the value over *with its quotes*, and a surrounding quote pair is
|
|
212
|
+
presented to the vendor as part of the secret.
|
|
213
|
+
- A trailing CR from a Windows-edited file is the same class of accident: the CR and everything
|
|
214
|
+
after it would travel as part of the secret.
|
|
215
|
+
|
|
216
|
+
### What happens when a value is malformed
|
|
217
|
+
|
|
218
|
+
| Path | Behaviour |
|
|
219
|
+
|------|-----------|
|
|
220
|
+
| `stdio` boot (env) | **boot refusal**, exit 1, naming the variable, the anomaly, the length and the load recipe — never a character of the value. The value is loaded through the SDK's own canonical loader (`loadAutotaskCredentialsFromEnv`), so this process has exactly one parser: the shell-equivalent one that strips a surrounding quote pair, drops a CRLF's CR and trims edge whitespace. |
|
|
221
|
+
| `http` per request | **before any client is constructed**, the three `X-Autotask-*` identity headers (and the impersonation header when present) are shape-checked. A malformed one answers the typed `CREDENTIAL_MALFORMED` envelope: `category: "validation"`, `retryable: false`, the anomaly names, and a `suggestedAction` naming the env-load pitfall. **Zero vendor wire** — nothing was dialed, so a `401` shape would be a lie, and `retryable: false` is what stops a retry loop against a lock-sensitive account. |
|
|
222
|
+
| vendor `401` | the envelope gains `credentialShape: "anomalous"` + the SDK's value-free `shapeDiagnostic` + the load-recipe `suggestedAction` when the credential this request presented shows a shape anomaly; `code`, `category`, `httpStatus`, `retryable` and `message` are unchanged. With **clean** values the envelope is byte-identical to the pinned `UNAUTHORIZED` one. |
|
|
223
|
+
|
|
224
|
+
Anomaly names reported: `surrounding_single_quotes`, `surrounding_double_quotes`, `carriage_return`,
|
|
225
|
+
`newline`, `edge_whitespace`. **Values are never printed** — lengths, two-character prefixes and
|
|
226
|
+
anomaly names only. `AUTOTASK_SECRET_READS=deny` and every other gate are unchanged, and the shape
|
|
227
|
+
check runs before them, at the boundary.
|
|
228
|
+
|
|
229
|
+
### `doctor` — the zero-wire preflight
|
|
230
|
+
|
|
231
|
+
`autotask-mcp doctor` (stdio CLI, exit `0`/`1`) and `GET /doctor` (http, same trust model as
|
|
232
|
+
`/health` and `/ready`: pre-auth, before the MCP handler) report the same facts about the
|
|
233
|
+
**server-held** environment: per field `present` / `len` / `prefix2` / `shapeOk` (+ the anomaly
|
|
234
|
+
names), the `AUTOTASK_BASE_URL` resolution (origin only), the host-policy mode and the SDK
|
|
235
|
+
plan-hash gate status. Nothing here emits a character of a secret: `AUTOTASK_SECRET`'s prefix is
|
|
236
|
+
masked to `[REDACTED]` (the identifiers keep their two-character format marker, which is not secret
|
|
237
|
+
material).
|
|
238
|
+
|
|
239
|
+
- **Zero vendor dials, ever** — `src/doctor.ts` imports no Autotask client and performs no network
|
|
240
|
+
read; the `dialed: 0` field is a property of the implementation, not a counter. Pinned by
|
|
241
|
+
`test/unit/doctor.test.ts` and `test/unit/doctor-route.test.ts` (a global `fetch` trap around the
|
|
242
|
+
whole route).
|
|
243
|
+
- **The contract:** run `doctor` before a live session. **A green doctor makes any later 401
|
|
244
|
+
vendor-side** — a rotated, revoked or role-restricted credential — rather than a mangled load.
|
|
245
|
+
A red doctor means fix the load recipe first; do not dial to "test" it.
|
|
246
|
+
- Under `http` the *tool-call* credential arrives per request in the `X-Autotask-*` headers and is
|
|
247
|
+
checked at that boundary, one request at a time; `doctor` reports the server-held environment
|
|
248
|
+
only and does not echo a caller's value back.
|
|
249
|
+
|
|
250
|
+
### Error model (the credential branch)
|
|
251
|
+
|
|
252
|
+
| Code | Category | Retryable | When |
|
|
253
|
+
|------|----------|-----------|------|
|
|
254
|
+
| `CREDENTIAL_MISSING` | `auth` | no | no credential on the request and no server-held default, or a partial tuple (all-or-nothing) |
|
|
255
|
+
| `CREDENTIAL_MALFORMED` | `validation` | no | a credential value's **shape** cannot be right (quote pair / CR / edge whitespace) — refused before any client is constructed, zero wire; `anomalies` lists the artifacts and `suggestedAction` names the env-load pitfall |
|
|
256
|
+
| `UNAUTHORIZED` | `auth` | no | the vendor rejected a credential that **was** sent (401/403). With anomalous values this envelope is annotated (`credentialShape`, `shapeDiagnostic`, `suggestedAction`); with clean values it is exactly the fixed envelope |
|
|
257
|
+
| `HOST_POLICY_DENIED` | `policy` | no | the origin is absent/unusable/not allow-listed (never the value echoed) |
|
|
258
|
+
|
|
259
|
+
## Per-request credential model
|
|
260
|
+
|
|
261
|
+
### http — per-request headers
|
|
262
|
+
|
|
263
|
+
Under `http`, the caller's Autotask identity arrives **per request** in fixed, non-configurable
|
|
264
|
+
headers — the wire contract the gateway is built to send:
|
|
265
|
+
|
|
266
|
+
| Header | Required | Meaning |
|
|
267
|
+
|--------|----------|---------|
|
|
268
|
+
| `X-Autotask-Api-Integration-Code` | yes (for tool calls) | the caller's integration code |
|
|
269
|
+
| `X-Autotask-Username` | yes (for tool calls) | the caller's username |
|
|
270
|
+
| `X-Autotask-Secret` | yes (for tool calls) | the caller's secret |
|
|
271
|
+
| `X-Autotask-Impersonation-Resource-Id` | no | the caller's impersonation target |
|
|
272
|
+
| `X-Autotask-Base-Url` | no | the caller's zone origin; without it the deployment default origin applies |
|
|
273
|
+
|
|
274
|
+
Discipline (security review S1/S2 — BLOCKING controls):
|
|
275
|
+
|
|
276
|
+
- **Single-valued rule:** a credential or zone header presented **twice** is treated as *absent*,
|
|
277
|
+
never "first value wins" (picking one is how request-smuggling bugs start).
|
|
278
|
+
- **Missing required credential ⇒ typed refusal, fail-closed.** The request resolves to a
|
|
279
|
+
credential-less client that refuses every tool call with `CREDENTIAL_MISSING`
|
|
280
|
+
(category `auth`, `retryable: false`), **pre-request, zero vendor wire**. `initialize` and
|
|
281
|
+
`tools/list` are still served without credentials (catalog sync for gateways).
|
|
282
|
+
- **No per-field default fill.** A caller can never splice its own header with a server-held
|
|
283
|
+
value: a **partial credential tuple** (one or two of the three identity headers) is malformed —
|
|
284
|
+
it is refused as `CREDENTIAL_MISSING` (the `partial_credential` kind), never completed from a
|
|
285
|
+
server-held default, which is all-or-nothing. The only other default that may exist is the
|
|
286
|
+
non-secret **origin** (`AUTOTASK_BASE_URL`).
|
|
287
|
+
- **Machine codes.** Credential refusals (absent or partial tuple) answer
|
|
288
|
+
`CREDENTIAL_MISSING` (category `auth`, `retryable: false`); origin refusals (no origin and no
|
|
289
|
+
default, unparseable/non-https/credential-bearing origin, host not admitted by the allow-list)
|
|
290
|
+
answer `HOST_POLICY_DENIED` (category `policy`). Both are fixed, value-free envelopes — the
|
|
291
|
+
refused value is never echoed. A scope that cannot resolve at all answers from a fail-closed
|
|
292
|
+
placeholder client built against a reserved RFC 6761 origin (`no-autotask-origin.invalid`):
|
|
293
|
+
pre-request, zero vendor wire.
|
|
294
|
+
- **Optional discouraged server defaults:** when the three credential vars are set under `http`,
|
|
295
|
+
every request *without* its own headers runs as that identity — and that identity is presented
|
|
296
|
+
**only to the default origin** (a request naming another origin with no headers of its own gets
|
|
297
|
+
no credential and fails closed). Set them only when that is intended; per-request headers are the
|
|
298
|
+
normal path.
|
|
299
|
+
- **Never in tool schemas:** no tool input schema exposes a credential, impersonation or base-url
|
|
300
|
+
field; model-supplied credentials are a protocol violation the server never accepts.
|
|
301
|
+
- **Host allow-list:** a caller-named `X-Autotask-Base-Url` is refused unless `AUTOTASK_ALLOWED_BASE_HOSTS`
|
|
302
|
+
permits it (exact host or `*.suffix`). Unparseable URLs are refused before reaching the SDK;
|
|
303
|
+
literal private-IP origins are always refused. Layered on top, per pooled client: the SDK
|
|
304
|
+
`hostPolicy` keeps the literal-IP floor and its `allowedOrigins` cover **this request's**
|
|
305
|
+
admitted caller origin plus the deployment default (the allow-list *admits* a caller origin;
|
|
306
|
+
the per-request `hostPolicy` is the construction-time floor). This allow-list is the
|
|
307
|
+
tenant-isolation control: a caller's headers are only
|
|
308
|
+
ever presented to an origin its allow-list entry (or the deployment default) names.
|
|
309
|
+
|
|
310
|
+
### stdio — environment
|
|
311
|
+
|
|
312
|
+
Under `stdio` the process environment **is** the trust boundary: the three credentials + zone are
|
|
313
|
+
required at boot (`AUTOTASK_API_INTEGRATION_CODE`, `AUTOTASK_USERNAME`, `AUTOTASK_SECRET`,
|
|
314
|
+
`AUTOTASK_BASE_URL`; optional `AUTOTASK_IMPERSONATION_RESOURCE_ID`); one process-lifetime client is
|
|
315
|
+
built from them — no pool. Missing required env is a **boot failure**: the process exits non-zero
|
|
316
|
+
with a typed config error naming the variable — never a value (fail-fast, before any listener).
|
|
317
|
+
|
|
318
|
+
Credential hygiene (both transports): the three credential values are **never logged, never written
|
|
319
|
+
to disk, never cached across requests except via the sanctioned pool key (a SHA-256 digest — raw
|
|
320
|
+
credentials never in keys, logs or eviction messages), never in error messages, and never in any
|
|
321
|
+
tool input schema**. Logging redacts the credential header names. On a vendor `401` (or an
|
|
322
|
+
impersonation-scope `403`) the pooled client entry is deleted — a stale identity is never
|
|
323
|
+
negative-cached or replayed.
|
|
324
|
+
|
|
325
|
+
## Per-tenant pool & cache
|
|
326
|
+
|
|
327
|
+
The stateless default is amended by one **user-approved deviation** (SCOPING Stage-4, 2026-10-06;
|
|
328
|
+
security decision D-1 ratified): a bounded **in-process** client pool with a per-tenant TTL read
|
|
329
|
+
cache, because a fresh client per request would silently disable the SDK's local vendor-budget
|
|
330
|
+
guards (rate limiter, 429 cooldown) — a worse security posture, not a safer one.
|
|
331
|
+
|
|
332
|
+
- **Pool** — `createClientPool` from `node-autotask/pool`: bounded LRU (default `maxSize` 128; the
|
|
333
|
+
server may lower it). Key = SHA-256 digest of the **normalised credential tuple**
|
|
334
|
+
(user, integration code, secret, resolved base URL, impersonation id). Optional idle TTL (lazy,
|
|
335
|
+
no timers). Eviction is logged without values. A vendor `401` (or an impersonation-scope `403`)
|
|
336
|
+
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.
|
|
340
|
+
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).
|
|
342
|
+
- **Boundaries — never relaxed:** in-process only (never on disk), **per-tenant** (one tenant's
|
|
343
|
+
cached records or limiter state can never answer another tenant's request), TTL-bounded, cleared
|
|
344
|
+
on eviction and on `401`. The same boundary covers the limiter/cooldown state.
|
|
345
|
+
- **stdio** uses one process-lifetime client (the env is the whole identity) — no pool, and the
|
|
346
|
+
cache/limiter state is that single tenant's by construction.
|
|
347
|
+
- **Multi-process gateways:** pool/cache/limiter state is per-process; an N-process gateway runs N
|
|
348
|
+
independent trackers (documented consequence, security review C7.2).
|
|
349
|
+
|
|
350
|
+
## Security posture
|
|
351
|
+
|
|
352
|
+
- **Redaction on by default, no opt-out surface:** the SDK redacts response PII/confidential fields
|
|
353
|
+
and always redacts `password`/`secretKey`/`secret`. The server must not and cannot disable it.
|
|
354
|
+
- **Untrusted-content wrapping:** vendor free text in results stays wrapped in the SDK's
|
|
355
|
+
`<untrusted_content>` markers end-to-end (prompt-injection boundary — data, never instructions);
|
|
356
|
+
tool descriptions carry the "treat as data" line.
|
|
357
|
+
- **Error bodies never echoed:** vendor 4xx/5xx bodies (which can echo request payloads carrying
|
|
358
|
+
`password`/`secretKey`) are not returned to MCP clients; tool errors carry the typed
|
|
359
|
+
code/category/retryable/correlationId envelope with a redaction-safe message.
|
|
360
|
+
- **Host allow-list is fail-closed at boot** (see [Per-request credential model](#per-request-credential-model)).
|
|
361
|
+
- **Sensitive-data-safe structured JSON logging:** NDJSON; stdio → stderr only (stdout is the
|
|
362
|
+
protocol), http → stdout. Operation keys, status, latency and byte sizes are loggable; credential
|
|
363
|
+
values, vendor bodies, full tool input bags, PII fields and the impersonation id at `info`
|
|
364
|
+
(boolean at `info`, id value at `debug` only — decision D-2) are never logged.
|
|
365
|
+
- **Correlation IDs:** server-generated random ids (never derived from caller content), threaded
|
|
366
|
+
through logs, structured MCP errors and response headers; cancellation/timeout propagates into
|
|
367
|
+
SDK calls via `AbortSignal` so an abandoned request stops consuming vendor budget.
|
|
368
|
+
- **Container:** non-root, multi-stage, production dependencies only (lockfile-based install — the
|
|
369
|
+
committed `pnpm-lock.yaml`/`pnpm-workspace.yaml` pin the tree; S8/C8.2), no baked secrets,
|
|
370
|
+
liveness-only healthcheck (no vendor call), direct `SIGTERM` (stop accepting, 2 s bounded drain of
|
|
371
|
+
in-flight work, exit 0), TLS termination at the gateway on a private unpublished network.
|
|
372
|
+
- **Supply chain:** `node-autotask` is the only **vendor** runtime dependency (the other two are
|
|
373
|
+
`@modelcontextprotocol/server` and `zod`); the pre-publish checklist
|
|
374
|
+
(manual — this repo has no CI) asserts it carries **zero runtime dependencies** and **no install
|
|
375
|
+
lifecycle hooks**, asserts the resolved `@modelcontextprotocol/server` version is ≥ 2.3.0 (the
|
|
376
|
+
baseline that serves the era pair on one endpoint), runs a production dependency audit, and
|
|
377
|
+
scans the image. This repo ships no GitHub Actions workflow; quality gates and publishing are
|
|
378
|
+
run manually by the maintainer.
|
|
379
|
+
|
|
380
|
+
Full control set: `.squad/security.md` (S1–S13, blocking B1–B9, decisions D-1–D-4, verification V1–V31).
|
|
381
|
+
|
|
382
|
+
## SDK dependency
|
|
383
|
+
|
|
384
|
+
`iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.8.3`** (an exact pin —
|
|
385
|
+
no range operator; subpaths `.` / `/capabilities` / `/operations` / `/resources` / `/types` /
|
|
386
|
+
`/pool`) — the only **vendor** dependency; the other two runtime dependencies are
|
|
387
|
+
`@modelcontextprotocol/server` (the MCP framework) and `zod` (schema validation). It never
|
|
388
|
+
depends on a cloned checkout — no `file:`, `git:`, `link:` or workspace references. Range
|
|
389
|
+
operators are prohibited on this dependency: a fresh lockfile-less install (every `npx`/`npm`
|
|
390
|
+
user — the published tarball ships no lockfile) must resolve to the release this surface was
|
|
391
|
+
reviewed against, or the plan-hash boot check refuses to start.
|
|
392
|
+
|
|
393
|
+
**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
|
|
395
|
+
exact pin and the committed `pnpm-lock.yaml` (with `pnpm-workspace.yaml`) pin the real published
|
|
396
|
+
tarball with its integrity hash, so installs resolve to exactly that release — the lockfile
|
|
397
|
+
freezes the pnpm/git/Docker paths, the pin freezes the npm/npx paths. The docs are written against this
|
|
398
|
+
published-package baseline, and the pre-publish checklist still starts with a registry re-check
|
|
399
|
+
(`npm view node-autotask version`) — which now passes.
|
|
400
|
+
|
|
401
|
+
**Plan-hash drift gate.** At boot, before the listener binds, the installed `node-autotask`'s
|
|
402
|
+
capability plan hash must equal the pin this surface was built and reviewed against
|
|
403
|
+
(`assertPlanHash`); a mismatch refuses the boot naming the drifted groups. The server only runs
|
|
404
|
+
against the SDK build it was reviewed against — a plan-changing SDK update can reach a user only
|
|
405
|
+
through a deliberately re-pinned server release, so drift is refused at boot, not discovered
|
|
406
|
+
mid-run.
|
|
407
|
+
|
|
408
|
+
**Upgrading node-autotask** is a deliberate server release (pin bump, plan-hash re-pin, mirrored-surface
|
|
409
|
+
re-review, gates) — not an npm-range float. The exact steps live in the [per-release
|
|
410
|
+
checklist](CONTRIBUTING.md#per-release-checklist-node-autotask-upgrade) in CONTRIBUTING.md, and `pnpm
|
|
411
|
+
fresh-install-check` re-proves the fresh-install property before every publish.
|
|
412
|
+
|
|
413
|
+
## Development
|
|
414
|
+
|
|
415
|
+
- Node **24** (current LTS; the SDK requires `node >= 24`), **pnpm 10**. The `pnpm-lock.yaml` is
|
|
416
|
+
committed with the first `node-autotask` publish (S8/C8.2); until then installs resolve without
|
|
417
|
+
a lockfile and the install step says so explicitly (reproducibility via the committed lockfile
|
|
418
|
+
is a pre-publish requirement; gates run manually — no CI in this repo).
|
|
419
|
+
- `pnpm i` — install · `pnpm build` — compile to `dist/` · `pnpm typecheck` · `pnpm lint`
|
|
420
|
+
- Tests: `pnpm test` (all) or per suite: `pnpm exec vitest run test/unit` / `test/integration` /
|
|
421
|
+
`test/conformance`. **No live vendor API and no credentials are ever required** — every suite
|
|
422
|
+
runs in mock mode (fixture-backed SDK boundary, no mock branches in tool handlers).
|
|
423
|
+
- `pnpm dev` runs the server from source via `tsx`.
|
|
424
|
+
|
|
425
|
+
Full contributor conventions: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
426
|
+
|
|
427
|
+
## Container execution
|
|
428
|
+
|
|
429
|
+
See [Gateway (http, container)](#gateway-http-container). The image is multi-stage (build with dev
|
|
430
|
+
toolchain → runtime with production dependencies only), non-root (`USER node`), and configured
|
|
431
|
+
purely through environment variables at run time. Probes: `/health` (liveness, in-image
|
|
432
|
+
`HEALTHCHECK`), `/ready` (readiness), `/metadata` (identity) — orchestrator probes may use any of
|
|
433
|
+
the three; none of them call the vendor.
|
|
434
|
+
|
|
435
|
+
## npm CLI execution
|
|
436
|
+
|
|
437
|
+
The npm package is an **executable CLI** (single canonical executable `autotask-mcp`), not a
|
|
438
|
+
reusable library API. Flags: `--transport` (`stdio` | `http`, default from `MCP_TRANSPORT` or
|
|
439
|
+
`stdio`) / `--host` (default all interfaces) / `--port` (default from `PORT` or `8080`) /
|
|
440
|
+
`--version` / `--help`. No credential flags exist; an unknown flag is refused (exit 2, usage
|
|
441
|
+
printed). A bind failure (e.g. port in use) exits non-zero with the problem named.
|
|
442
|
+
|
|
443
|
+
```
|
|
444
|
+
npx -y iit-mcp-autotask doctor # zero-wire preflight (exit 0 = go; see §Credentials)
|
|
445
|
+
npx -y iit-mcp-autotask --transport http --port 8080
|
|
446
|
+
npx -y iit-mcp-autotask --transport stdio
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
`doctor` is a **subcommand**, matched before any config work beyond parsing: it prints the
|
|
450
|
+
credential shape report, the base-url/allow-list resolution and the plan-hash gate status, dials
|
|
451
|
+
nothing, and exits `0` when everything would be accepted / `1` otherwise (a runbook step
|
|
452
|
+
`doctor && live-run` therefore stops rather than dialing a credential the report says is
|
|
453
|
+
malformed).
|
|
454
|
+
|
|
455
|
+
## Gateway assumptions
|
|
456
|
+
|
|
457
|
+
The shared MCP gateway is the control plane. It owns: client authentication, per-tenant tool
|
|
458
|
+
filtering/exposure, **credential injection** into the `X-Autotask-*` headers, rate limiting, and
|
|
459
|
+
body-size/compression limits. The server is tenant-agnostic, exposes no deployment-specific
|
|
460
|
+
identity of its own, and adds no client-level rate limiting of its own — the vendor-side budget
|
|
461
|
+
(10,000 req/hour, ≤10 concurrent per integration) is the server's only rate concern, guarded by
|
|
462
|
+
the SDK agent profile (8,000/hr local budget, 3 concurrent) and exposed live through
|
|
463
|
+
`autotask_threshold_information`.
|
|
464
|
+
|
|
465
|
+
## Known SDK gaps
|
|
466
|
+
|
|
467
|
+
None of the ten confirmed workflows is blocked. Confirmed absent from the SDK (recommend SDK
|
|
468
|
+
issues — do not implement in this server):
|
|
469
|
+
|
|
470
|
+
- **Knowledge base** — `KnowledgeBaseArticles` (43 paths) + `KnowledgeBaseCategories` (12): a real
|
|
471
|
+
SDK gap vs the vendored spec.
|
|
472
|
+
- **Warehouse inventory** — `InventoryItems` / `InventoryLocations` / `InventoryStockedItems{,Add,Remove,Transfer}` /
|
|
473
|
+
`InventoryTransfers` / `InventoryItemSerialNumbers` (the SDK covers `InventoryProducts` only).
|
|
474
|
+
- **Documents/articles ecosystem** — `Document*`/`Article*` family (the whole KB/document layer).
|
|
475
|
+
- **Sales & pricing** — `SalesOrders`, `PriceList*`, `Taxes`/`TaxCategories`/`TaxRegions`,
|
|
476
|
+
`Currencies`, `PaymentTerms`, `ShippingTypes`, standalone `ServiceBundles`.
|
|
477
|
+
- **Attachment downloads** — metadata-only surfaces for company/project/task/resource/expense
|
|
478
|
+
parents (the SDK covers `ticket_attachments` + `time_entry_attachments` downloads only).
|
|
479
|
+
|
|
480
|
+
Not in the vendor REST spec at all (vendor surface gaps, not SDK gaps): reporting/BI, estimates,
|
|
481
|
+
physical assets (Autotask models them as Configuration Items — covered).
|
|
482
|
+
|
|
483
|
+
## Known limitations & blockers
|
|
484
|
+
|
|
485
|
+
| # | Item | Status |
|
|
486
|
+
|---|------|--------|
|
|
487
|
+
| B1 | **Project phase & task lists** (W7 sub-capability): no `project_tasks`/`project_phases` operations in the SDK's 1,212-op registry. W7 ships with project status + context; phase/task lists excluded and documented. Classification (SDK gap vs vendor API-version limit) is decided by recon's spec-side check (A2). | blocked — documented |
|
|
488
|
+
| B2 | **Time-entry writes on ww29/australia-nz for API-only users**: vendor 500 (live-verified). `autotask_timeEntry_create` works where the API user is a real resource; the wall is documented in the tool description so agents don't loop. | zone limitation |
|
|
489
|
+
| B3 | **`time_entries.getPosted`/`getUnposted` 500 on ww29** (no posted-state filter on the zone's entity). Reachable via the generic layer; `postedWithin` is the zone-robust alternative. | zone limitation |
|
|
490
|
+
| B4 | **CI create on tenants without products** (ww29 product wall: `products.count = 0`): `configuration_items.create` requires an existing `productID`. No dedicated CI-create in v1; the wall is documented on the generic side. | zone limitation |
|
|
491
|
+
| B5 | **Blind count/query surfaces on ww29** (companies not owned by the caller; tickets): count/query return 0/empty even when data exists. Not fixable client-side; the surface carries the typed blind-surface sentinel — `get(id)`/full-profile are the authorities. | zone limitation |
|
|
492
|
+
| B6 | **`node-autotask` npm publish**: the Phase-C install gate re-checks the registry; if still 404 the build pauses and escalates (SCOPING-resolved: the user publishes — no git/file dependency fallback). | open — user action |
|
|
493
|
+
|
|
494
|
+
Zone-anchored curation note (recon F5): several description pins are facts observed on ww29
|
|
495
|
+
(australia-nz) and are carried as zone-labelled pins, never silently deleted; on other tenants
|
|
496
|
+
they may be zone/user-specific.
|