iit-mcp-hudu 0.2.3 → 0.3.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.
Files changed (63) hide show
  1. package/.env.example +13 -42
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +92 -56
  4. package/dist/auth.js +0 -1
  5. package/dist/base-hosts.js +0 -1
  6. package/dist/config.js +24 -44
  7. package/dist/credential.js +0 -1
  8. package/dist/doctor.js +0 -1
  9. package/dist/http.js +0 -1
  10. package/dist/lifecycle.js +0 -1
  11. package/dist/logger.js +0 -1
  12. package/dist/main.js +23 -1
  13. package/dist/policy.js +59 -9
  14. package/dist/stdio.js +0 -1
  15. package/dist/tools/article-loss.js +61 -0
  16. package/dist/tools/company-context-policy.js +0 -1
  17. package/dist/tools/context.js +0 -1
  18. package/dist/tools/dedicated.js +55 -79
  19. package/dist/tools/dispatch.js +25 -13
  20. package/dist/tools/fetch-many.js +3 -4
  21. package/dist/tools/lists.js +117 -160
  22. package/dist/tools/meta.js +3 -6
  23. package/dist/tools/reads.js +37 -36
  24. package/dist/tools/register.js +0 -1
  25. package/dist/tools/renamed.js +13 -0
  26. package/dist/tools/result-urls.js +0 -1
  27. package/dist/tools/results.js +7 -1
  28. package/dist/tools/schemas.js +5 -3
  29. package/dist/tools/search-policy.js +0 -1
  30. package/dist/tools/search-prose.js +5 -3
  31. package/dist/tools/search.js +0 -2
  32. package/dist/tools/server.js +25 -24
  33. package/dist/tools/slim.js +34 -0
  34. package/dist/tools.js +1 -2
  35. package/package.json +9 -10
  36. package/dist/auth.d.ts +0 -51
  37. package/dist/base-hosts.d.ts +0 -20
  38. package/dist/config.d.ts +0 -95
  39. package/dist/credential.d.ts +0 -67
  40. package/dist/doctor.d.ts +0 -64
  41. package/dist/http.d.ts +0 -60
  42. package/dist/lifecycle.d.ts +0 -29
  43. package/dist/logger.d.ts +0 -24
  44. package/dist/main.d.ts +0 -2
  45. package/dist/policy.d.ts +0 -79
  46. package/dist/stdio.d.ts +0 -21
  47. package/dist/tools/company-context-policy.d.ts +0 -16
  48. package/dist/tools/context.d.ts +0 -31
  49. package/dist/tools/dedicated.d.ts +0 -123
  50. package/dist/tools/dispatch.d.ts +0 -4
  51. package/dist/tools/fetch-many.d.ts +0 -42
  52. package/dist/tools/lists.d.ts +0 -49
  53. package/dist/tools/meta.d.ts +0 -7
  54. package/dist/tools/reads.d.ts +0 -7
  55. package/dist/tools/register.d.ts +0 -13
  56. package/dist/tools/result-urls.d.ts +0 -2
  57. package/dist/tools/results.d.ts +0 -180
  58. package/dist/tools/schemas.d.ts +0 -119
  59. package/dist/tools/search-policy.d.ts +0 -165
  60. package/dist/tools/search-prose.d.ts +0 -79
  61. package/dist/tools/search.d.ts +0 -6
  62. package/dist/tools/server.d.ts +0 -77
  63. package/dist/tools.d.ts +0 -34
package/.env.example CHANGED
@@ -57,27 +57,21 @@ MCP_TRANSPORT=stdio
57
57
  # caller that can reach the listener is trusted.
58
58
  # HUDU_ALLOW_ANY_BASE_HOST=false
59
59
 
60
- # Omit mutation tools entirely by default. Set false before enabling a write policy.
61
- #
62
- # Deployment spectrum — the variables below compose it, and every step stays fail-closed:
63
- # light read-only : the shipped defaults (HUDU_READ_ONLY=true, HUDU_WRITE_POLICY=deny,
64
- # HUDU_SECRET_READS=deny). Reads only; no mutation tools at all.
65
- # scoped writes : HUDU_READ_ONLY=false, HUDU_WRITE_POLICY=allow_list,
66
- # HUDU_WRITE_ALLOW=<operation keys> (plus HUDU_SECRET_READS=allow if
67
- # plaintext credential reads are wanted).
68
- # full read-write : HUDU_READ_ONLY=false, HUDU_WRITE_POLICY=all (plus HUDU_SECRET_READS=allow).
69
- # Every write and destructive operation is permitted; the SDK's per-mode
70
- # client gates (read / write / destructive) still apply underneath.
71
- HUDU_READ_ONLY=true
72
- # HUDU_READ_ONLY=false
73
-
74
- # Authority over the mutation dispatchers. deny (the default) refuses every write and destructive
75
- # operation; allow_list permits only the keys in HUDU_WRITE_ALLOW; all is today's behaviour and has
76
- # to be typed out deliberately. An unset value means deny, so forgetting this gets a read-only
77
- # server, never an open one. Reads are never affected.
60
+ # Deployment spectrum -- HUDU_WRITE_POLICY is the one switch for mutation, and every step is fail-closed:
61
+ # read-only : HUDU_WRITE_POLICY=deny (default). No hudu_write / hudu_delete tool exists.
62
+ # scoped writes : HUDU_WRITE_POLICY=allow_list + HUDU_WRITE_ALLOW=<operation keys>. hudu_write is
63
+ # registered only if the list has a write op, hudu_delete only if it has a destructive op.
64
+ # full read-write : HUDU_WRITE_POLICY=all. The SDK's per-mode client gates still apply underneath.
65
+ # Plaintext credential reads are a separate switch: HUDU_SECRET_READS=allow.
66
+
67
+ # Authority over the mutation dispatchers. deny (the default) registers no mutation tool; allow_list permits only the keys in HUDU_WRITE_ALLOW; all is today's behaviour and has
68
+ # to be typed out deliberately. Unset means deny. Reads are never affected.
78
69
  HUDU_WRITE_POLICY=deny
79
70
  # HUDU_WRITE_POLICY=allow_list
80
71
  # HUDU_WRITE_POLICY=all
72
+ # Hard override: true means no write/delete tool and every mutation refused, whatever the policy and
73
+ # allow-list say. Unset or false leaves HUDU_WRITE_POLICY in charge.
74
+ # HUDU_READ_ONLY=true
81
75
 
82
76
  # Authority over reads that return a plaintext credential -- independent of HUDU_WRITE_POLICY, which
83
77
  # never covers them: asset_passwords.get returns password and otp_secret as an ordinary read. deny
@@ -91,6 +85,7 @@ HUDU_SECRET_READS=deny
91
85
  # Only read under HUDU_WRITE_POLICY=allow_list. Comma-separated registry operation keys, e.g.
92
86
  # articles.update,assets.create. It must be non-empty and every key must exist in the registry (and
93
87
  # be a write) or the container fails at boot with the variable named. Ignored under deny/all.
88
+ # Allowing a write also allows the writes it delegates to (articles.patchSection -> articles.update).
94
89
  # The write client is offered the write-effect keys and the delete client the destructive-effect
95
90
  # keys from this one list: to reach the same coverage as `all` via an allow list, it must name
96
91
  # every write and every destructive operation (the full key set is the node-hudu capability
@@ -101,30 +96,6 @@ HUDU_SECRET_READS=deny
101
96
  PORT=8787
102
97
  LOG_LEVEL=info
103
98
 
104
- # Pages the search body index walks per resource (SDK default 250 = 25,000 records). Lower it when
105
- # the tenant's assets are numerous enough to evict article bodies from the 64 MB text budget: the
106
- # article walk must fit with room to spare or every search answers body-blind. 40 covers ~4,000
107
- # articles and caps assets at ~4,000.
108
- HUDU_SEARCH_MAX_INDEX_PAGES=40
109
-
110
- # The read-through cache the SDK clients use (SDK 0.12.0). on (the default) selects the SDK's
111
- # per-entity TTL preset (1 h structural / 5 min mutable / 1 min base): repeated reads of the
112
- # same record are served from the cache until the TTL expires. off disables it: every read
113
- # re-dials Hudu.
114
- # HUDU_CACHE_PRESET=on
115
-
116
- # The 429 cooldown gate (SDK 0.12.0). on (the default) arms a local deadline after a vendor
117
- # 429 whose retries are spent (the profile's 1 retry: the first such call dials twice): calls
118
- # the gate refuses are answered locally with notSent: true, never re-dialed. off
119
- # retries a 429 per the agent profile's retry budget instead of arming a local deadline.
120
- # HUDU_COOLDOWN=on
121
-
122
- # The whole-call deadline in milliseconds (SDK 0.12.0). Must be a positive integer. Defaults
123
- # to 30000 and is always sent to the SDK explicitly, so it stays above the agent profile's
124
- # 20 s deadline regardless of preset: the default keeps the tolerance a cold tier:'index'
125
- # body-index walk had before the 0.12.0 adoption.
126
- # HUDU_TIMEOUT_MS=30000
127
-
128
99
  # The resources hudu_search and hudu_resolve_any may offer and query. Unset means the SDK's full
129
100
  # searchable set (articles, asset_passwords, assets, companies, groups, password_folders, users,
130
101
  # websites). Set it to the subset THIS deployment's Hudu API key can read, so a tool neither
package/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ **Upgrading from 0.2.3: breaking changes.** (1) Article bodies through `hudu_read` / `hudu_write` / `hudu_delete` are Markdown by default. (2) `HUDU_CACHE_PRESET`, `HUDU_COOLDOWN`, `HUDU_TIMEOUT_MS` and `HUDU_SEARCH_MAX_INDEX_PAGES` are removed and a set one now fails boot. (3) `hudu_list_companies` / `_articles` / `_assets` / `_asset_layouts` are replaced by `hudu_list(resource)`. (4) Served tool definitions drop `_meta`. (5) The repository uses pnpm. The SDK is pinned exactly to node-hudu 0.16.0 (plan hash `fba0aa46...`); 0.15.0 was never published. `HUDU_READ_ONLY` is NOT removed: `true` is a hard override (see below). **UPGRADE HAZARD:** on 0.2.3 an UNSET `HUDU_READ_ONLY` meant read-only. Now only `HUDU_WRITE_POLICY` decides when it is unset, so these two configs, with `HUDU_READ_ONLY` unset, now give the model `hudu_write` / `hudu_delete`: (a) `HUDU_WRITE_POLICY=all`; (b) `HUDU_WRITE_POLICY=allow_list` with a non-empty `HUDU_WRITE_ALLOW`. Set `HUDU_READ_ONLY=true` to keep them read-only. The server logs a warning at boot in exactly this case.
6
+
7
+
8
+ - Behaviour change: `hudu_read` / `hudu_write` / `hudu_delete` serve and accept article bodies as Markdown (node-hudu `contentFormat: 'markdown'`). Bodies that open with block HTML are still sent as HTML.
9
+ - Tooling: the repository moves from npm to pnpm (`pnpm-lock.yaml`, `pnpm-workspace.yaml`, `packageManager`); the Dockerfile and CI use pnpm, and the runtime image carries no package manager. Contributors run `pnpm install`.
10
+ - The MCP handshake reports the package version (it was hardcoded 0.1.0).
11
+ - CLI: `--version` and `--help` work without configuration. The package no longer ships source maps or `.d.ts` files. The SDK's tracker references ("issues #70/#72") are stripped from served tool descriptions.
12
+ - CI: adds `pnpm audit --prod` and a container job (build, no package manager, fail-fast boot, non-default port, origin guard).
13
+ - Dependency: node-hudu 0.13.2 to 0.14.0 (Hudu Markdown dialect, `articles.getSection`, `articles.patchSection`); catalog plan hash and reachable-operation counts updated for the two new operations.
14
+ - Dependency: node-hudu 0.14.0 -> 0.16.0 (0.15.0 was never published). New discoverable operations: `articles.pin`, `articles.unpin`, `asset_passwords.listShares` / `getShare` / `createShare` / `deleteShare`; no served tool added. `asset_layouts.create` / `update` now send the body nested under `asset_layout` (SDK fix: fields were silently dropped). Plan hash re-stamped to `fba0aa46...`.
15
+ - `hudu_resolve_any` returns `unauthorized` (resources the credential may not read) beside the permitted hits; it errors only when every requested resource refuses. The dispatcher path returns it too.
16
+ - `hudu_get_company_context` accepts `opts.skip` and returns `skipped`.
17
+ - Share operations: refused under the default `HUDU_SECRET_READS=deny`; `createShare` is never runnable (the SDK needs `reveal`, this server never grants it) and is listed unreachable.
18
+ - Fix: the `hudu_search` default-tier sentence rewrite matched stale SDK text, so `auto` and narrowed deployments advertised a false `index` default; it now fails loudly if the anchor drifts.
19
+ - Discovery no longer names tools this server does not serve (`hudu_get_asset_password`, `hudu_get_website`).
20
+ - Served surface: `$schema` and safe-integer `maximum` stripped, SDK dispatcher field text served verbatim, shorter article-format note and `hudu_search` `query` text; an intermediate 46.3 / 40.8 KB. A size-gate test ratchets both.
21
+ - Mermaid is stored in the bare editor-native `<pre class="language-mermaid">` form (SDK 0.16.0); round-trip test added.
22
+ - Breaking: the four `hudu_list_companies` / `hudu_list_articles` / `hudu_list_assets` / `hudu_list_asset_layouts` tools are replaced by one `hudu_list` with a `resource` argument. Filters move inside `filters`; an unaccepted filter is refused, `asset_layouts` refuses `page_size`. A call to an old name answers with a pointer to `hudu_list`. Same bounds and pagination as before. Resync tool catalogs.
23
+ - Breaking (metadata): served tool definitions no longer carry `_meta` (`backingOperation`, `sensitive`, `requiresApproval`, ...).
24
+ - Served surface after these: 19 tools / 39.5 KB write-enabled, 17 tools / 33.9 KB read-only (`tools/list`), down from 22 / 50.1 KB and 20 / 43.7 KB on node-hudu 0.16.0.
25
+ - Markdown article reads (`hudu_read` `articles.get` / `getContext` / `resolve`, `hudu_get_article_context`) that lose content the dialect cannot express (e.g. an inline `<svg>`) now set `markdown_lossy: true` and `markdown_lossy_codes` and add one sentence to the result text; a lossless read is byte-identical to before. Detection uses the SDK's `diffArticleRoundTrip` (content-impact findings only).
26
+ - `allow_list` registers `hudu_write` only when the list holds a write-effect operation and `hudu_delete` only when it holds a destructive-effect one (previously both). `all` registers both; `deny` and `HUDU_READ_ONLY=true` neither. The boot log states which were registered.
27
+ - `HUDU_WRITE_POLICY` (`deny` default | `allow_list` | `all`) is the write switch; `deny` registers no `hudu_write` / `hudu_delete` tool. Allowing a write also allows the writes it delegates to (`articles.patchSection` -> `articles.update`). `HUDU_READ_ONLY=true` stays a HARD OVERRIDE: no write or delete tool and every mutation refused, whatever `HUDU_WRITE_POLICY` / `HUDU_WRITE_ALLOW` say; unset or `false` leaves `HUDU_WRITE_POLICY` in charge. The effective policy is logged once at boot.
28
+ - Behaviour note: on 0.2.3 an UNSET `HUDU_READ_ONLY` defaulted to true (read-only). It is now unset = `HUDU_WRITE_POLICY` decides, so a deployment that sets `HUDU_WRITE_POLICY=allow_list` or `all` without `HUDU_READ_ONLY=true` gets the write tools. Set `HUDU_READ_ONLY=true` to keep a deployment read-only regardless.
29
+ - Breaking: `HUDU_CACHE_PRESET`, `HUDU_COOLDOWN`, `HUDU_TIMEOUT_MS` and `HUDU_SEARCH_MAX_INDEX_PAGES` are removed; the SDK client now uses the fixed agent profile, recommended cache, a 30 s timeout and an armed cooldown. A set one fails boot.
30
+ - `hudu_get_article_context` returns Markdown; `hudu_read`, `hudu_write`, `hudu_list` and `hudu_fetch_many` descriptions state which paths speak Markdown and which carry stored HTML.
31
+ - Tests: Markdown read and write loss-guard coverage through the dispatchers.
package/README.md CHANGED
@@ -3,29 +3,32 @@
3
3
  MCP server for Hudu IT documentation, built on the [`node-hudu`](https://www.npmjs.com/package/node-hudu) SDK.
4
4
 
5
5
  ## Tool surface
6
-
7
- 20 tools by default: `hudu_list_operations`, `hudu_describe_operation`, `hudu_read`,
8
- `hudu_fetch_many`, twelve core reads, and four bounded list reads. Setting
9
- `HUDU_READ_ONLY=false` adds `hudu_write` and `hudu_delete` (22 tools). Each dispatcher uses a
10
- separate SDK client in its matching mode,
11
- with names, annotations and operation enums from the SDK's curated metadata. SDK enforcement
12
- rejects cross-effect calls before requests. The old `hudu_invoke`, `hudu_catalog` and
13
- `hudu_describe` names are removed; consumers must resync their tool catalog.
14
-
15
- The four list tools each wrap exactly ONE fixed registry operation (`companies.list` /
16
- `articles.list` / `assets.listAcrossCompanies` / `asset_layouts.list`), take exactly one page and
17
- accept no operation selector or nested invocation, so an ungated Reader can page companies,
18
- articles, assets or asset layouts without using a registry dispatcher and without exposing the write
19
- surface. The first three take `page` / `page_size` (capped at 100); `hudu_list_asset_layouts` takes
20
- `page` only, because `/asset_layouts` ignores `page_size` — a caller bound would truncate a vendor
21
- page and skip rows as `page` advanced, so the tool returns the vendor page whole and page N is
22
- always the vendor's Nth page. For the searchable resources (`companies` / `articles` /
23
- `assets`) a resource excluded by `HUDU_SEARCH_RESOURCES` is refused by the list tool too, before any
24
- request; `asset_layouts` is not searchable (the SDK declares no vendor text filter for it), so
25
- `hudu_list_asset_layouts` follows the same reachability as the existing ungated
26
- `hudu_get_asset_layout` tool.
27
-
28
- These tools run on node-hudu 0.13.2; the `SDK behaviour (0.12.0)` section below covers the behaviours that shape them: redirect refusal, the closed input contract, the error envelope, and the client opt-ins. Numeric ids must be positive integers on every tool; anything else is refused before a request is made.
6
+ 17 tools by default: `hudu_list_operations`, `hudu_describe_operation`, `hudu_read`,
7
+ `hudu_fetch_many`, twelve core reads, and `hudu_list`. Setting `HUDU_WRITE_POLICY` `allow_list` or
8
+ `all` adds `hudu_write` and `hudu_delete` (19 tools). Each dispatcher uses a separate SDK client in
9
+ its matching mode; names, annotations and the dispatcher field text are the SDK's curated metadata.
10
+ SDK enforcement rejects cross-effect calls before any request. The old `hudu_invoke`,
11
+ `hudu_catalog` and `hudu_describe` names are removed; consumers must resync their tool catalog.
12
+
13
+ `hudu_list(resource)` replaces the four `hudu_list_companies` / `hudu_list_articles` /
14
+ `hudu_list_assets` / `hudu_list_asset_layouts` tools of 0.2.x (a call to an old name answers with a
15
+ pointer to `hudu_list`). `resource` selects one of four fixed registry operations (`companies.list` /
16
+ `articles.list` / `assets.listAcrossCompanies` / `asset_layouts.list`); the tool takes exactly one
17
+ page and accepts no operation selector or nested invocation, so an ungated reader can page these
18
+ collections without the registry dispatcher and without exposing the write surface. Filters go in
19
+ `filters`; each resource accepts only its own set, and any other key is refused naming the accepted
20
+ ones (never dropped, which would turn a company-scoped request into an account-wide one). Companies,
21
+ articles and assets take `page` / `page_size` (capped at 100); `asset_layouts` takes `page` only,
22
+ because `/asset_layouts` ignores `page_size`: a caller bound would truncate the vendor page and skip
23
+ rows as `page` advanced, so the tool returns the vendor page whole and page N is always the
24
+ vendor's Nth page. For searchable resources (`companies` / `articles` / `assets`) a resource
25
+ excluded by `HUDU_SEARCH_RESOURCES` is refused by the list tool too, before any request;
26
+ `asset_layouts` is not searchable (the SDK declares no vendor text filter for it), so it follows the
27
+ same reachability as the existing ungated `hudu_get_asset_layout` tool. The tool set is the SDK's
28
+ `CORE_TOOLS` plus `hudu_list`, which is this server's own; a future SDK upgrade must not re-add the
29
+ four old names.
30
+
31
+ These tools run on node-hudu 0.16.0; the `SDK behaviour (0.12.0)` section below covers the behaviours that shape them: redirect refusal, the closed input contract, the error envelope, and the client opt-ins. Numeric ids must be positive integers on every tool; anything else is refused before a request is made.
29
32
 
30
33
  `hudu_fetch_many` (node-hudu 0.12.0) fetches up to 20 single records across mixed resources in
31
34
  one call: each item is exactly `{ resource, id, fields? }` — one of the 22 bare-id resources, a
@@ -33,7 +36,7 @@ numeric vendor id, and an optional client-side field projection. A shape violati
33
36
  the schema layer before any request, and failure is per-item: a miss or a vendor fault lands on
34
37
  its own row as a typed error while the rest of the batch is served. The deployment's secret-read
35
38
  policy applies to the batch's items exactly as to a single read, so a credential item is
36
- refused pre-request under the default `HUDU_SECRET_READS=deny`; `HUDU_READ_ONLY` keeps the tool
39
+ refused pre-request under the default `HUDU_SECRET_READS=deny`; `HUDU_WRITE_POLICY=deny` keeps the tool
37
40
  — it is a read. The same batch stays reachable through `hudu_read` with operation
38
41
  `operations.fetchMany`, gated the same way.
39
42
 
@@ -47,7 +50,7 @@ refused pre-request under the default `HUDU_SECRET_READS=deny`; `HUDU_READ_ONLY`
47
50
  ## Local use (stdio)
48
51
 
49
52
  Needs Node 24+, a Hudu API key and your Hudu origin. `stdio` is the default transport, so no
50
- `MCP_TRANSPORT` is needed. Mutation tools stay off unless you set `HUDU_READ_ONLY=false`.
53
+ `MCP_TRANSPORT` is needed. Mutation tools stay off unless you set `HUDU_WRITE_POLICY`.
51
54
 
52
55
  **From npm** (recommended):
53
56
 
@@ -74,7 +77,7 @@ or in `.mcp.json` / Claude Desktop config:
74
77
  `npx` fetches the package on first run and caches it; use `iit-mcp-hudu@<version>` to pin one.
75
78
 
76
79
  **From a clone** (for development): see [Development](#development). Point your client at
77
- `node /absolute/path/to/iit-mcp-hudu/dist/main.js` after `npm run build`.
80
+ `node /absolute/path/to/iit-mcp-hudu/dist/main.js` after `pnpm run build`.
78
81
 
79
82
  ## Loading credential values (read this before a live session)
80
83
 
@@ -121,7 +124,7 @@ whose envelope names a shape anomaly (`credentialShape: "anomalous"`) points bac
121
124
  The server enforces the shape itself, so a bad load cannot reach the wire even if `doctor` is
122
125
  skipped: a malformed `HUDU_API_KEY` fails boot; a malformed `X-Hudu-Api-Key` header is refused
123
126
  before any client is built, with the typed `CREDENTIAL_MALFORMED` envelope (value-free, and
124
- `retryable: false`). Neither path dials. Since node-hudu 0.13.2 the SDK enforces credential shape
127
+ `retryable: false`). Neither path dials. Since node-hudu 0.14.0 the SDK enforces credential shape
125
128
  itself too — refusing a contaminated value at client construction and attaching a value-free shape
126
129
  diagnostic to a refused credential — and this server consumes that diagnostic directly on a 401;
127
130
  the server's own gate stays as the boundary guard, a superset of the SDK's checks.
@@ -176,16 +179,21 @@ every tool call, with the error naming the missing header. A
176
179
  credential header sent twice (which Node would otherwise join with `", "`) is treated as absent
177
180
  for the same reason.
178
181
 
179
- `HUDU_READ_ONLY=true` is the default and omits both mutation tools entirely, regardless of
180
- write policy. To enable mutations, explicitly set it to `false` and configure
181
- `HUDU_WRITE_POLICY`: `deny` rejects all mutations, `allow_list` permits only the canonical
182
- keys in `HUDU_WRITE_ALLOW`, and `all` permits the SDK's mutation surface. An empty or invalid
183
- allow-list fails startup. The SDK clients receive the same allow-list plus reads needed by
184
- helper dependencies; client mode and effect dispatch remain independent restrictions.
182
+ `HUDU_WRITE_POLICY` is the write switch. `deny` (the default) omits `hudu_write` and
183
+ `hudu_delete` entirely; `allow_list` permits only the canonical keys in `HUDU_WRITE_ALLOW` and
184
+ registers `hudu_write` only if the list holds at least one write-effect operation, and `hudu_delete`
185
+ only if it holds at least one destructive-effect one (per the SDK registry's effect) (a write allowed here also allows the writes it delegates to: allowing
186
+ `articles.patchSection` allows `articles.update`); `all` permits the SDK's whole mutation surface.
187
+ An empty or invalid allow-list fails startup. The SDK clients receive the same allow-list plus
188
+ reads needed by helper dependencies.
185
189
  Writes default to dry-run previews; execution requires `dry_run:false`, and destructive
186
190
  operations additionally require exact `confirm`. A confirmation string is not human consent.
187
191
  Discovery reports deployment-policy refusals as unreachable.
188
192
 
193
+ `HUDU_READ_ONLY=true` is a hard override of the write policy: no `hudu_write` / `hudu_delete` tool is
194
+ registered and every mutation is refused, whatever `HUDU_WRITE_POLICY` and `HUDU_WRITE_ALLOW` say. Unset or
195
+ `false` leaves `HUDU_WRITE_POLICY` in charge (default `deny`). The effective policy is logged once at boot.
196
+
189
197
  `HUDU_WRITE_POLICY` stays a **deployment-wide** decision, not a per-caller one. Per-caller
190
198
  credentials make each caller's Hudu role an external authority too, but that authority is not ours
191
199
  to trust and cannot be allowed to widen this server's surface: the server-side policy is the floor,
@@ -211,6 +219,19 @@ without `expand`. It is independent of `HUDU_WRITE_POLICY`: `all` does not imply
211
219
  `allow` does not open writes. `hudu_search` is unaffected — it already redacts hits on those two
212
220
  resources. Refusals are `POLICY_DENIED` and appear as unreachable rows in `hudu_list_operations`.
213
221
 
222
+ Password share links (node-hudu 0.16.0): `asset_passwords.listShares`, `getShare`, `createShare` and
223
+ `deleteShare` are credential-resource operations, so they are refused under the default
224
+ `HUDU_SECRET_READS=deny`; the two writes also need `HUDU_WRITE_POLICY` (or `HUDU_WRITE_ALLOW`) to permit
225
+ them. Under `allow`, `listShares` / `getShare` work and the share `url` (a bearer link) is masked.
226
+ `createShare` is never runnable here: the SDK refuses it (`POLICY_DENIED`, `reveal_required`, dry-run
227
+ included, no request) without its `reveal` capability, this server never grants `reveal`, and
228
+ `hudu_list_operations` marks the row unreachable. Link-shaped text is scrubbed from share errors.
229
+
230
+ `hudu_get_company_context` accepts `opts.skip` (`assets`, `articles`, `websites`, `asset_passwords`); pass
231
+ `["asset_passwords"]` when the key cannot read credentials. The skipped list comes back empty with
232
+ `skipped` set, and no request is made for it. `hudu_resolve_any` returns `unauthorized` (the resources the
233
+ credential may not read) beside the permitted hits; it errors only when every requested resource refuses.
234
+
214
235
  All SDK clients keep default redaction enabled, including when `HUDU_SECRET_READS=allow`.
215
236
  That flag permits credential-resource operations; it never enables plaintext results. The
216
237
  SDK masks credential fields and confidential or unclassified custom fields as `[REDACTED]`.
@@ -238,17 +259,35 @@ background, so a caller's later search is body-aware without the first one await
238
259
  walk (some clients enforce a 60s call timeout). Under `stdio` the one long-lived index stays warm and the
239
260
  default is `"index"`. Pass `tier:"index"` explicitly for a complete first answer.
240
261
 
241
- Three 0.12.0 preset knobs are env-tunable, each defaulting to the adopted behaviour.
242
- `HUDU_CACHE_PRESET` (`on` | `off`) toggles the read-through cache: the default `on` selects
243
- the SDK's `recommended` per-entity TTL preset, and `off` disables it, so repeated reads
244
- re-dial Hudu instead of being served from the cache.
245
- `HUDU_COOLDOWN` (`on` | `off`) is the 429 cooldown gate: `off` means a 429 retries per the
246
- agent profile instead of arming a local deadline whose refusals answer with `notSent: true`.
247
- `HUDU_TIMEOUT_MS` (positive integer milliseconds) is the whole-call deadline, default `30000`;
248
- it is always sent to the SDK explicitly, so it stays above the profile's 20 s.
262
+ The read-through cache (SDK `recommended` preset), the 429 cooldown gate and the 30 s whole-call deadline are fixed, not env-tunable.
249
263
  `blockPrivateHosts` deliberately gets no override: it is the literal-IP SSRF floor for
250
264
  caller-named origins — a security property, not a deployment preference.
251
265
 
266
+ ## Article bodies are Markdown (node-hudu 0.14+)
267
+
268
+ `hudu_read` and `hudu_write` pass `contentFormat: 'markdown'` to the SDK, and `hudu_get_article_context`
269
+ asks for Markdown, so single-article bodies (`articles.get`, `articles.getContext`, `articles.resolve`,
270
+ `create`, `update`) are returned and accepted as Markdown. Hudu callouts (`> [!INFO]`), accordions, task
271
+ lists and Mermaid fences (stored as the editor-native `<pre class="language-mermaid">` form) round-trip as the dialect documents. Rows from `hudu_list` (articles),
272
+ `hudu_fetch_many`, `articles.list`, `articles.search` and `articles.findBySlug` still carry stored HTML:
273
+ those operations take no format. Rules a caller needs:
274
+
275
+ - A write's `data.content` is parsed as Markdown unless it opens with block-level HTML (`<p>`, `<div>`,
276
+ `<h1>`-`<h6>`, `<table>`, lists, `<pre>`, `<blockquote>`, `<hr>`, Hudu's `mce-accordion`), in which case
277
+ it is sent as HTML. Re-read an article through `articles.get` before editing it.
278
+ - A Markdown write is refused when the stored article holds something the dialect cannot express
279
+ (for example an inline `<svg>`); send HTML to keep it. The SDK's loss guard runs on every write.
280
+ - Prefer Markdown (or editor-native HTML): Hudu reshapes API-written HTML on the next UI save (h5/h6 flattened, `small`/`abbr`/`q`/`section`/`aside` unwrapped, `thead` removed, an unknown callout type becomes info).
281
+ - `articles.getSection` / `articles.patchSection` read or edit one section as Markdown, with dry-run.
282
+ - A Markdown read can be incomplete: anything the dialect cannot express (an inline `<svg>`, say) comes
283
+ back as plain text. `hudu_read` (`articles.get` / `getContext` / `resolve`) and
284
+ `hudu_get_article_context` then set `markdown_lossy: true` (with `markdown_lossy_codes`, the SDK's
285
+ `diffArticleRoundTrip` content-loss codes) and add one sentence to the result text; fetch the stored
286
+ HTML with `hudu_fetch_many` (`{items:[{resource:"articles", id}]}`) to see the full body. The read is one
287
+ fetch with HTML forced and converted locally (so there is no second request); a caller's own
288
+ `opts.format: "html"` returns the stored HTML untouched with no flag. Callouts, accordions, task
289
+ lists and Mermaid round-trip cleanly and are never flagged; a lossless read is unchanged.
290
+
252
291
  ## SDK behaviour (0.12.0)
253
292
 
254
293
  Three SDK behaviours changed in the 0.12.0 line and shape a deployment:
@@ -284,13 +323,13 @@ Every client the server builds — the parent, the fail-closed placeholders and
284
323
  (1 retry, 240/min rate limit, concurrency 3, bounded resolution scans). The rate-limit bucket
285
324
  is per client, i.e. per scope. The preset stays a deployment-wide constant with no env
286
325
  override; the one deadline it would otherwise govern is pinned explicitly instead
287
- (`HUDU_TIMEOUT_MS`, below), and the 429 now arms the cooldown gate instead of retrying
326
+ (a fixed 30 s, below), and the 429 now arms the cooldown gate instead of retrying
288
327
  blindly.
289
- - `cache: 'recommended'` — env-toggled as `HUDU_CACHE_PRESET` (default `on`): the
328
+ - `cache: 'recommended'` — fixed on: the
290
329
  per-entity read-through TTL preset (1 h structural / 5 min mutable / 1 min base). Caches are
291
330
  per client, so per scope. A write through the write or delete client does not clear the read
292
331
  client's cache (separate instances), so a read after a mutation is TTL-bounded stale, never
293
- unbounded. `off` disables the read-through cache: repeated reads re-dial Hudu.
332
+ unbounded.
294
333
  - `blockPrivateHosts` on the per-scope clients: the literal-IP SSRF floor for CALLER-named
295
334
  origins, fail-closed at client construction. It complements the `HUDU_ALLOWED_BASE_HOSTS`
296
335
  hostname matcher (which alone can express wildcard suffixes) and holds even in
@@ -299,20 +338,17 @@ Every client the server builds — the parent, the fail-closed placeholders and
299
338
  env boundary and is never floor-checked, so an on-prem install whose Hudu sits on a literal
300
339
  private IP is unaffected by the upgrade. Deliberately no env override: it is a security
301
340
  property, not a deployment preference.
302
- - `cooldown: { enabled: true }` — env-tunable as `HUDU_COOLDOWN` (default `on`): the 429
341
+ - `cooldown: { enabled: true }` — fixed on: the 429
303
342
  cooldown gate; a call refused locally by the gate answers with `notSent: true` in the error
304
343
  envelope, so a model does not read a local refusal as a vendor rejection of a sent call.
305
- `off` retries a 429 per the profile instead of arming a local deadline.
306
344
 
307
- The whole-call deadline is `HUDU_TIMEOUT_MS` (positive integer milliseconds, default `30000`):
345
+ The whole-call deadline is a fixed `30000` ms:
308
346
  it is always passed to the SDK explicitly, so it stays above the agent profile's 20 s
309
347
  regardless of preset — the preset's 20 s would shorten a cold `tier:'index'` body-index walk
310
348
  that the 0.9.2 deployment tolerated at the SDK's 30 s, and a cold-scope search must not start
311
349
  timing out because of an upgrade.
312
350
 
313
- These are deployment-wide, not per-caller: `HUDU_CACHE_PRESET`, `HUDU_COOLDOWN` and
314
- `HUDU_TIMEOUT_MS` are env-tunable with the defaults above, while `profile` and
315
- `blockPrivateHosts` stay constants.
351
+ These are deployment-wide constants, not per-caller and not env-tunable.
316
352
 
317
353
 
318
354
  ## Development
@@ -320,16 +356,16 @@ These are deployment-wide, not per-caller: `HUDU_CACHE_PRESET`, `HUDU_COOLDOWN`
320
356
  ```
321
357
  git clone https://github.com/msoukhomlinov/iit-mcp-hudu.git
322
358
  cd iit-mcp-hudu
323
- npm install
324
- npm run build
325
- npm test
326
- npm run dev
359
+ pnpm install
360
+ pnpm run build
361
+ pnpm test
362
+ pnpm run dev
327
363
  ```
328
364
 
329
365
  CI (`.github/workflows/ci.yml`) runs `typecheck`, `lint` and `test` on every push and pull request.
330
366
 
331
- `node-hudu` is pinned to `0.13.2`. The catalog hash test detects metadata drift; upgrades require
332
- review of the client authority, operation enums and redaction regressions. Docker uses `npm ci`.
367
+ `node-hudu` is pinned to `0.16.0`. The catalog hash test detects metadata drift; upgrades require
368
+ review of the client authority, operation enums and redaction regressions. Docker uses `pnpm install --frozen-lockfile`.
333
369
 
334
370
  ## Deployment
335
371
 
package/dist/auth.js CHANGED
@@ -81,4 +81,3 @@ export function headerRecord(headers) {
81
81
  });
82
82
  return out;
83
83
  }
84
- //# sourceMappingURL=auth.js.map
@@ -36,4 +36,3 @@ export function originOf(url) {
36
36
  return undefined;
37
37
  }
38
38
  }
39
- //# sourceMappingURL=base-hosts.js.map
package/dist/config.js CHANGED
@@ -135,18 +135,6 @@ const schema = z
135
135
  */
136
136
  HUDU_ALLOW_ANY_BASE_HOST: z.enum(['true', 'false']).default('false').transform((value) => value === 'true'),
137
137
  MCP_TRANSPORT: z.enum(TRANSPORTS).default('stdio'),
138
- /**
139
- * Pages the body index walks per resource per build (SDK default 250 = 25,000 records).
140
- *
141
- * Tenant-shaped, so it is a knob rather than a constant. The walk runs articles first and assets
142
- * second, and both compete for one 64 MB text budget that evicts least-recently-read text: on a
143
- * tenant with tens of thousands of assets the asset text arrives last, so the ARTICLE BODIES are
144
- * the first thing evicted — and article bodies are the only reason this index exists (assets
145
- * carry no body, and Hudu's own ?search= already covers their titles and fields). Sizing this so
146
- * the article walk completes with budget left over is what keeps searches from going body-blind.
147
- * A tenant with 3,000+ articles needs about 32 pages.
148
- */
149
- HUDU_SEARCH_MAX_INDEX_PAGES: z.coerce.number().int().min(1).max(1000).optional(),
150
138
  /**
151
139
  * The resources `hudu_search` may offer and query — the read-edge counterpart to
152
140
  * `HUDU_WRITE_POLICY`.
@@ -156,15 +144,21 @@ const schema = z
156
144
  * resources THIS credential can read so the tool never advertises a guaranteed-empty search and
157
145
  * a model never spends a turn on a resource the key is not allowed to read. Every name is
158
146
  * validated against the SDK's searchable set at boot, so a typo fails the container instead of a
159
- * call weeks later. Applies to `hudu_search` only; the dedicated resolve tools are unaffected.
147
+ * call weeks later. Applies to `hudu_search`, `hudu_resolve_any` and the same operations through `hudu_read`; the dedicated per-resource resolve tools are unaffected.
160
148
  */
161
149
  HUDU_SEARCH_RESOURCES: z.string().optional().transform(parseSearchResources),
162
150
  /**
163
- * Whether `hudu_read / hudu_write / hudu_delete` may reach a `write` or `destructive` operation at all. Read/`null`
164
- * effects are never touched by this policy. Defaults to `deny` — see {@link WRITE_POLICIES}.
151
+ * The single switch for mutation. `deny` (the default) registers no `hudu_write` / `hudu_delete`
152
+ * tool at all; `allow_list` and `all` register them and gate every call. Read effects are never
153
+ * touched. See {@link WRITE_POLICIES}.
165
154
  */
166
- HUDU_READ_ONLY: z.enum(['true', 'false']).default('true').transform((value) => value === 'true'),
167
155
  HUDU_WRITE_POLICY: z.enum(WRITE_POLICIES).default('deny'),
156
+ /**
157
+ * Hard override. `true` means no `hudu_write` / `hudu_delete` tool and no write or destructive
158
+ * operation, whatever `HUDU_WRITE_POLICY` / `HUDU_WRITE_ALLOW` say (see {@link applyReadOnly}).
159
+ * Unset or `false` leaves `HUDU_WRITE_POLICY` in charge (its default is `deny`).
160
+ */
161
+ HUDU_READ_ONLY: z.enum(['true', 'false']).optional().transform((value) => value === 'true'),
168
162
  /**
169
163
  * The operation keys `HUDU_WRITE_POLICY=allow_list` permits. Read only under `allow_list`;
170
164
  * ignored under `deny`/`all`. Exposed as the parsed array so no consumer re-splits the string.
@@ -183,33 +177,6 @@ const schema = z
183
177
  HUDU_SECRET_READS: z.enum(SECRET_READ_POLICIES).default('deny'),
184
178
  PORT: z.coerce.number().int().positive().max(65535).default(8787),
185
179
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
186
- /**
187
- * The read-through cache preset every client this server builds gets (SDK 0.12.0).
188
- *
189
- * The operator-facing toggle for the read-through cache. `on` (the default) selects the
190
- * SDK's `recommended` per-entity TTL preset (1 h structural / 5 min mutable / 1 min base);
191
- * `off` disables the read-through cache, so a repeated read re-dials Hudu instead of
192
- * being served from the cache.
193
- */
194
- HUDU_CACHE_PRESET: z.enum(['on', 'off']).default('on'),
195
- /**
196
- * The 429 cooldown gate (SDK 0.12.0).
197
- *
198
- * `on` (the default) arms a local deadline after a vendor 429 whose retries are SPENT (the
199
- * agent profile's 1 retry, so the first such call makes two wire attempts): calls the gate
200
- * then refuses locally, and the error envelope marks them `notSent: true` so a local
201
- * refusal stays distinguishable from a vendor rejection of a sent call. `off`
202
- * retries a 429 per the profile's retry budget instead of arming a local deadline.
203
- */
204
- HUDU_COOLDOWN: z.enum(['on', 'off']).default('on'),
205
- /**
206
- * The whole-call deadline in milliseconds (SDK 0.12.0). Positive integer.
207
- *
208
- * Defaults to 30000 and is always passed to the SDK explicitly, so it wins over the
209
- * agent profile's 20 s deadline regardless of preset; the default keeps the tolerance
210
- * a cold `tier:'index'` body-index walk had before the 0.12.0 adoption.
211
- */
212
- HUDU_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
213
180
  })
214
181
  .superRefine((c, ctx) => {
215
182
  // Pre-dial integrity check (2026-10-07 Autotask incident class): a credential whose bytes were
@@ -371,15 +338,28 @@ function checkCredentialShape(value, ctx, path) {
371
338
  * @throws {ConfigError} when any variable is missing or malformed. The message lists every problem
372
339
  * at once — a boot loop that surfaces one missing variable per restart wastes real minutes.
373
340
  */
341
+ /** Variables removed in 0.3.0. A stale one must fail boot rather than be ignored silently. */
342
+ const REMOVED_VARS = ['HUDU_CACHE_PRESET', 'HUDU_COOLDOWN', 'HUDU_TIMEOUT_MS', 'HUDU_SEARCH_MAX_INDEX_PAGES'];
374
343
  export function loadConfig(env = process.env) {
344
+ const stale = REMOVED_VARS.filter((name) => env[name] !== undefined);
345
+ if (stale.length > 0) {
346
+ throw new ConfigError(`Invalid configuration:\n ${stale.join(', ')}: removed in 0.3.0; delete it. HUDU_WRITE_POLICY (deny | allow_list | all) governs writes, with HUDU_READ_ONLY=true as a hard override.`);
347
+ }
375
348
  const parsed = schema.safeParse(env);
376
349
  if (parsed.success)
377
350
  return parsed.data;
378
351
  const problems = parsed.error.issues.map((i) => ` ${i.path.join('.') || '(config)'}: ${i.message}`);
379
352
  throw new ConfigError(`Invalid configuration:\n${problems.join('\n')}`);
380
353
  }
354
+ /**
355
+ * The configuration the server actually runs with: `HUDU_READ_ONLY=true` forces the policy to
356
+ * `deny` (no write or delete tool is registered and every write is refused), whatever
357
+ * `HUDU_WRITE_POLICY` / `HUDU_WRITE_ALLOW` say. Otherwise the config is returned unchanged.
358
+ */
359
+ export function applyReadOnly(config) {
360
+ return config.HUDU_READ_ONLY ? { ...config, HUDU_WRITE_POLICY: 'deny', HUDU_WRITE_ALLOW: [] } : config;
361
+ }
381
362
  /** Thrown only by {@link loadConfig}. Never carries a credential value — only variable names. */
382
363
  export class ConfigError extends Error {
383
364
  name = 'ConfigError';
384
365
  }
385
- //# sourceMappingURL=config.js.map
@@ -112,4 +112,3 @@ export function reportCredential(env, name) {
112
112
  return { present: false };
113
113
  return { present: true, shape: describeCredential(raw) };
114
114
  }
115
- //# sourceMappingURL=credential.js.map
package/dist/doctor.js CHANGED
@@ -76,4 +76,3 @@ export function runDoctor(config) {
76
76
  notes,
77
77
  };
78
78
  }
79
- //# sourceMappingURL=doctor.js.map
package/dist/http.js CHANGED
@@ -146,4 +146,3 @@ export function startHttpServer(config, mcp, log, exit = process.exit) {
146
146
  server.listen(config.PORT, () => log.info('listening', { port: config.PORT, transport: 'http' }));
147
147
  return server;
148
148
  }
149
- //# sourceMappingURL=http.js.map
package/dist/lifecycle.js CHANGED
@@ -49,4 +49,3 @@ export function installHttpLifecycle(server, log, exit = process.exit) {
49
49
  function describe(value) {
50
50
  return value instanceof Error ? (value.stack ?? value.message) : String(value);
51
51
  }
52
- //# sourceMappingURL=lifecycle.js.map
package/dist/logger.js CHANGED
@@ -23,4 +23,3 @@ export function createLogger(minLevel = 'info', write = (l) => process.stderr.wr
23
23
  };
24
24
  return { debug: at('debug'), info: at('info'), warn: at('warn'), error: at('error') };
25
25
  }
26
- //# sourceMappingURL=logger.js.map
package/dist/main.js CHANGED
@@ -5,8 +5,10 @@
5
5
  * Nothing else lives here. Both transports register the same tool surface from the same factory
6
6
  * (`createMcpServerFactory`), so "which transport" is the only decision this file makes.
7
7
  */
8
+ import { createRequire } from 'node:module';
8
9
  import { createMcpHandler } from '@modelcontextprotocol/server';
9
10
  import { ConfigError, loadConfig } from './config.js';
11
+ import { logEffectiveWritePolicy } from './policy.js';
10
12
  import { runDoctor } from './doctor.js';
11
13
  import { createLogger } from './logger.js';
12
14
  import { installHttpLifecycle } from './lifecycle.js';
@@ -43,7 +45,27 @@ function runDoctorCommand() {
43
45
  // the dial the report just said not to make.
44
46
  process.exit(report.ok ? 0 : 1);
45
47
  }
48
+ const USAGE = `iit-mcp-hudu: MCP server for Hudu IT documentation
49
+
50
+ Usage:
51
+ iit-mcp-hudu serve (MCP_TRANSPORT=stdio|http, configured by environment)
52
+ iit-mcp-hudu doctor zero-wire credential preflight
53
+ iit-mcp-hudu --version print the version
54
+ iit-mcp-hudu --help print this help
55
+
56
+ Configuration is environment-only; see .env.example in the package.
57
+ `;
46
58
  function main() {
59
+ // Answered before config is loaded, so they work with no credentials.
60
+ const flag = process.argv[2];
61
+ if (flag === '--version' || flag === '-v') {
62
+ process.stdout.write(`${createRequire(import.meta.url)('../package.json').version}\n`);
63
+ return;
64
+ }
65
+ if (flag === '--help' || flag === '-h') {
66
+ process.stdout.write(USAGE);
67
+ return;
68
+ }
47
69
  // `doctor` is matched before anything else, so it never builds a logger, a transport or a client.
48
70
  if (process.argv[2] === 'doctor') {
49
71
  runDoctorCommand();
@@ -64,6 +86,7 @@ function main() {
64
86
  throw err;
65
87
  }
66
88
  const log = createLogger(config.LOG_LEVEL);
89
+ logEffectiveWritePolicy(config, log, process.env);
67
90
  if (config.MCP_TRANSPORT === 'http'
68
91
  && config.HUDU_BASE_URL === undefined
69
92
  && config.HUDU_ALLOWED_BASE_HOSTS === undefined) {
@@ -89,4 +112,3 @@ function main() {
89
112
  installHttpLifecycle(server, log);
90
113
  }
91
114
  main();
92
- //# sourceMappingURL=main.js.map