iit-mcp-hudu 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +13 -42
- package/CHANGELOG.md +46 -0
- package/README.md +99 -56
- package/dist/auth.js +0 -1
- package/dist/base-hosts.js +0 -1
- package/dist/config.js +24 -44
- package/dist/credential.js +0 -1
- package/dist/doctor.js +0 -1
- package/dist/http.js +0 -1
- package/dist/lifecycle.js +0 -1
- package/dist/logger.js +0 -1
- package/dist/main.js +23 -1
- package/dist/policy.js +59 -9
- package/dist/stdio.js +0 -1
- package/dist/tools/article-loss.js +46 -0
- package/dist/tools/company-context-policy.js +0 -1
- package/dist/tools/context.js +0 -1
- package/dist/tools/dedicated.js +56 -79
- package/dist/tools/dispatch.js +34 -12
- package/dist/tools/fetch-many.js +7 -4
- package/dist/tools/lists.js +136 -159
- package/dist/tools/meta.js +3 -6
- package/dist/tools/reads.js +37 -36
- package/dist/tools/register.js +0 -1
- package/dist/tools/renamed.js +13 -0
- package/dist/tools/result-urls.js +0 -1
- package/dist/tools/results.js +29 -8
- package/dist/tools/schemas.js +5 -3
- package/dist/tools/search-policy.js +0 -1
- package/dist/tools/search-prose.js +5 -3
- package/dist/tools/search.js +4 -3
- package/dist/tools/server.js +90 -63
- package/dist/tools/slim.js +34 -0
- package/dist/tools.js +1 -2
- package/package.json +9 -10
- package/dist/auth.d.ts +0 -51
- package/dist/base-hosts.d.ts +0 -20
- package/dist/config.d.ts +0 -95
- package/dist/credential.d.ts +0 -67
- package/dist/doctor.d.ts +0 -64
- package/dist/http.d.ts +0 -60
- package/dist/lifecycle.d.ts +0 -29
- package/dist/logger.d.ts +0 -24
- package/dist/main.d.ts +0 -2
- package/dist/policy.d.ts +0 -79
- package/dist/stdio.d.ts +0 -21
- package/dist/tools/company-context-policy.d.ts +0 -16
- package/dist/tools/context.d.ts +0 -31
- package/dist/tools/dedicated.d.ts +0 -123
- package/dist/tools/dispatch.d.ts +0 -4
- package/dist/tools/fetch-many.d.ts +0 -42
- package/dist/tools/lists.d.ts +0 -49
- package/dist/tools/meta.d.ts +0 -7
- package/dist/tools/reads.d.ts +0 -7
- package/dist/tools/register.d.ts +0 -13
- package/dist/tools/result-urls.d.ts +0 -2
- package/dist/tools/results.d.ts +0 -180
- package/dist/tools/schemas.d.ts +0 -119
- package/dist/tools/search-policy.d.ts +0 -165
- package/dist/tools/search-prose.d.ts +0 -79
- package/dist/tools/search.d.ts +0 -6
- package/dist/tools/server.d.ts +0 -77
- 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
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
#
|
|
68
|
-
#
|
|
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,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
**Upgrading from 0.3.0:** `hudu_list` now returns compact summary rows by default. Callers that read custom `fields`, article `content` or layout `fields` from list rows must pass `expand: true`. Everything else is additive or a fix.
|
|
6
|
+
|
|
7
|
+
- Docs: `articles.getSection` carries no `markdown_lossy` signal (node-hudu drops `content_loss` inside `readSection`); the README now says so and a test pins it. Read the whole article with `articles.get` to learn whether a body is lossy.
|
|
8
|
+
- Behaviour change: `hudu_list` serves compact summary rows by default (the node-hudu `*Summary` field sets, plus `url` / `created_at` / `updated_at`) instead of full records, cutting a default page from ~49-88k to under 20k characters. `expand: true` returns the full records as before. Envelope, `page_size` default 25 and `asset_layouts` paging (no `page_size`) are unchanged. Callers that read custom `fields`, article `content` or layout `fields` from list rows must pass `expand: true` (issue #22).
|
|
9
|
+
- Fix: the 401 shape annotation is bound to the call (AsyncLocalStorage) instead of module-level state written per request, so under http a 401 never carries another concurrent request's credential anomalies (#16).
|
|
10
|
+
- Fix: a 401 is annotated only with the anomalies of the credential actually presented on that call (the request key wins over the server-held `HUDU_API_KEY`; the env key's anomalies join only when it is the one dialed), and `hudu_search`'s fully-refused branch now carries the same annotation as every other tool (#16).
|
|
11
|
+
- Fix: `hudu_search` and `hudu_read` `operations.searchKnowledge` / `operations.searchAcrossResources` now refuse a malformed `x-hudu-api-key` (`CREDENTIAL_MALFORMED`) and an origin outside the allow-list (the `AUTH_ERROR` envelope other tools return) before any request, instead of returning a successful 0-hit result (#32).
|
|
12
|
+
- Fix: the same search paths now refuse every other refusing client state (key with no origin, invalid base URL, keyless request) with the `AUTH_ERROR` envelope, and `hudu_fetch_many` / `hudu_read` `operations.fetchMany` return that top-level refusal for a whole-batch credential or origin refusal instead of `ok: 0, failed: 1` with per-item errors; a genuine per-item vendor failure still lands on its row. A keyless `hudu_search` is no longer a soft 0-hit result (#35).
|
|
13
|
+
- Dependency: node-hudu 0.16.0 -> 0.17.0. Plan hash re-stamped to `1ad5e23b...`; operation set unchanged (236).
|
|
14
|
+
- Markdown article reads take their loss signal from the SDK: node-hudu now attaches `content_loss` to the record on a Markdown read, so the server no longer forces HTML and converts and diffs the body itself. `markdown_lossy` / `markdown_lossy_codes` are unchanged; the per-record `content_loss` is lifted into them and not passed through.
|
|
15
|
+
- Behaviour change: inline `<svg>` and `<iframe>` now survive Markdown reads and writes (they were flagged lossy and writes over them were refused). A body that converts to nothing reads as an empty body flagged `ROUNDTRIP_BODY_EMPTY` instead of failing.
|
|
16
|
+
- Docs: decision record for per-user identity (`docs/decisions/per-user-identity.md`): do not build the Hudu account link now; Entra sign-in plus server-side per-user policy and audit instead.
|
|
17
|
+
|
|
18
|
+
## 0.3.0
|
|
19
|
+
|
|
20
|
+
**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.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
- 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`.
|
|
25
|
+
- The MCP handshake reports the package version (it was hardcoded 0.1.0).
|
|
26
|
+
- 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.
|
|
27
|
+
- CI: adds `pnpm audit --prod` and a container job (build, no package manager, fail-fast boot, non-default port, origin guard).
|
|
28
|
+
- 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.
|
|
29
|
+
- 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...`.
|
|
30
|
+
- `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.
|
|
31
|
+
- `hudu_get_company_context` accepts `opts.skip` and returns `skipped`.
|
|
32
|
+
- 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.
|
|
33
|
+
- 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.
|
|
34
|
+
- Discovery no longer names tools this server does not serve (`hudu_get_asset_password`, `hudu_get_website`).
|
|
35
|
+
- 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.
|
|
36
|
+
- Mermaid is stored in the bare editor-native `<pre class="language-mermaid">` form (SDK 0.16.0); round-trip test added.
|
|
37
|
+
- 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.
|
|
38
|
+
- Breaking (metadata): served tool definitions no longer carry `_meta` (`backingOperation`, `sensitive`, `requiresApproval`, ...).
|
|
39
|
+
- 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.
|
|
40
|
+
- 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).
|
|
41
|
+
- `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.
|
|
42
|
+
- `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.
|
|
43
|
+
- 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.
|
|
44
|
+
- 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.
|
|
45
|
+
- `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.
|
|
46
|
+
- Tests: Markdown read and write loss-guard coverage through the dispatchers.
|
package/README.md
CHANGED
|
@@ -3,29 +3,35 @@
|
|
|
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
|
-
|
|
8
|
-
`
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
`articles.list` / `assets.listAcrossCompanies` / `asset_layouts.list`)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`
|
|
24
|
-
|
|
25
|
-
`
|
|
26
|
-
`
|
|
27
|
-
|
|
28
|
-
|
|
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. Rows are compact summaries by default (id, name, key scalar fields, `url`,
|
|
30
|
+
`created_at`, `updated_at`; no custom-field values, article bodies or layout field definitions), which keeps a
|
|
31
|
+
25-row assets page under 20,000 characters instead of ~88,000; pass `expand: true` for the full
|
|
32
|
+
records (`hudu_read` `articles.list` rows are always full). Article rows carry no body unless expanded. Paging is unchanged (`asset_layouts` still takes no `page_size`).
|
|
33
|
+
|
|
34
|
+
These tools run on node-hudu 0.17.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
35
|
|
|
30
36
|
`hudu_fetch_many` (node-hudu 0.12.0) fetches up to 20 single records across mixed resources in
|
|
31
37
|
one call: each item is exactly `{ resource, id, fields? }` — one of the 22 bare-id resources, a
|
|
@@ -33,7 +39,7 @@ numeric vendor id, and an optional client-side field projection. A shape violati
|
|
|
33
39
|
the schema layer before any request, and failure is per-item: a miss or a vendor fault lands on
|
|
34
40
|
its own row as a typed error while the rest of the batch is served. The deployment's secret-read
|
|
35
41
|
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`; `
|
|
42
|
+
refused pre-request under the default `HUDU_SECRET_READS=deny`; `HUDU_WRITE_POLICY=deny` keeps the tool
|
|
37
43
|
— it is a read. The same batch stays reachable through `hudu_read` with operation
|
|
38
44
|
`operations.fetchMany`, gated the same way.
|
|
39
45
|
|
|
@@ -47,7 +53,7 @@ refused pre-request under the default `HUDU_SECRET_READS=deny`; `HUDU_READ_ONLY`
|
|
|
47
53
|
## Local use (stdio)
|
|
48
54
|
|
|
49
55
|
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 `
|
|
56
|
+
`MCP_TRANSPORT` is needed. Mutation tools stay off unless you set `HUDU_WRITE_POLICY`.
|
|
51
57
|
|
|
52
58
|
**From npm** (recommended):
|
|
53
59
|
|
|
@@ -74,7 +80,7 @@ or in `.mcp.json` / Claude Desktop config:
|
|
|
74
80
|
`npx` fetches the package on first run and caches it; use `iit-mcp-hudu@<version>` to pin one.
|
|
75
81
|
|
|
76
82
|
**From a clone** (for development): see [Development](#development). Point your client at
|
|
77
|
-
`node /absolute/path/to/iit-mcp-hudu/dist/main.js` after `
|
|
83
|
+
`node /absolute/path/to/iit-mcp-hudu/dist/main.js` after `pnpm run build`.
|
|
78
84
|
|
|
79
85
|
## Loading credential values (read this before a live session)
|
|
80
86
|
|
|
@@ -121,7 +127,7 @@ whose envelope names a shape anomaly (`credentialShape: "anomalous"`) points bac
|
|
|
121
127
|
The server enforces the shape itself, so a bad load cannot reach the wire even if `doctor` is
|
|
122
128
|
skipped: a malformed `HUDU_API_KEY` fails boot; a malformed `X-Hudu-Api-Key` header is refused
|
|
123
129
|
before any client is built, with the typed `CREDENTIAL_MALFORMED` envelope (value-free, and
|
|
124
|
-
`retryable: false`). Neither path dials. Since node-hudu 0.
|
|
130
|
+
`retryable: false`). Neither path dials. Since node-hudu 0.14.0 the SDK enforces credential shape
|
|
125
131
|
itself too — refusing a contaminated value at client construction and attaching a value-free shape
|
|
126
132
|
diagnostic to a refused credential — and this server consumes that diagnostic directly on a 401;
|
|
127
133
|
the server's own gate stays as the boundary guard, a superset of the SDK's checks.
|
|
@@ -176,16 +182,21 @@ every tool call, with the error naming the missing header. A
|
|
|
176
182
|
credential header sent twice (which Node would otherwise join with `", "`) is treated as absent
|
|
177
183
|
for the same reason.
|
|
178
184
|
|
|
179
|
-
`
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
`HUDU_WRITE_POLICY` is the write switch. `deny` (the default) omits `hudu_write` and
|
|
186
|
+
`hudu_delete` entirely; `allow_list` permits only the canonical keys in `HUDU_WRITE_ALLOW` and
|
|
187
|
+
registers `hudu_write` only if the list holds at least one write-effect operation, and `hudu_delete`
|
|
188
|
+
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
|
|
189
|
+
`articles.patchSection` allows `articles.update`); `all` permits the SDK's whole mutation surface.
|
|
190
|
+
An empty or invalid allow-list fails startup. The SDK clients receive the same allow-list plus
|
|
191
|
+
reads needed by helper dependencies.
|
|
185
192
|
Writes default to dry-run previews; execution requires `dry_run:false`, and destructive
|
|
186
193
|
operations additionally require exact `confirm`. A confirmation string is not human consent.
|
|
187
194
|
Discovery reports deployment-policy refusals as unreachable.
|
|
188
195
|
|
|
196
|
+
`HUDU_READ_ONLY=true` is a hard override of the write policy: no `hudu_write` / `hudu_delete` tool is
|
|
197
|
+
registered and every mutation is refused, whatever `HUDU_WRITE_POLICY` and `HUDU_WRITE_ALLOW` say. Unset or
|
|
198
|
+
`false` leaves `HUDU_WRITE_POLICY` in charge (default `deny`). The effective policy is logged once at boot.
|
|
199
|
+
|
|
189
200
|
`HUDU_WRITE_POLICY` stays a **deployment-wide** decision, not a per-caller one. Per-caller
|
|
190
201
|
credentials make each caller's Hudu role an external authority too, but that authority is not ours
|
|
191
202
|
to trust and cannot be allowed to widen this server's surface: the server-side policy is the floor,
|
|
@@ -211,6 +222,19 @@ without `expand`. It is independent of `HUDU_WRITE_POLICY`: `all` does not imply
|
|
|
211
222
|
`allow` does not open writes. `hudu_search` is unaffected — it already redacts hits on those two
|
|
212
223
|
resources. Refusals are `POLICY_DENIED` and appear as unreachable rows in `hudu_list_operations`.
|
|
213
224
|
|
|
225
|
+
Password share links (node-hudu 0.16.0): `asset_passwords.listShares`, `getShare`, `createShare` and
|
|
226
|
+
`deleteShare` are credential-resource operations, so they are refused under the default
|
|
227
|
+
`HUDU_SECRET_READS=deny`; the two writes also need `HUDU_WRITE_POLICY` (or `HUDU_WRITE_ALLOW`) to permit
|
|
228
|
+
them. Under `allow`, `listShares` / `getShare` work and the share `url` (a bearer link) is masked.
|
|
229
|
+
`createShare` is never runnable here: the SDK refuses it (`POLICY_DENIED`, `reveal_required`, dry-run
|
|
230
|
+
included, no request) without its `reveal` capability, this server never grants `reveal`, and
|
|
231
|
+
`hudu_list_operations` marks the row unreachable. Link-shaped text is scrubbed from share errors.
|
|
232
|
+
|
|
233
|
+
`hudu_get_company_context` accepts `opts.skip` (`assets`, `articles`, `websites`, `asset_passwords`); pass
|
|
234
|
+
`["asset_passwords"]` when the key cannot read credentials. The skipped list comes back empty with
|
|
235
|
+
`skipped` set, and no request is made for it. `hudu_resolve_any` returns `unauthorized` (the resources the
|
|
236
|
+
credential may not read) beside the permitted hits; it errors only when every requested resource refuses.
|
|
237
|
+
|
|
214
238
|
All SDK clients keep default redaction enabled, including when `HUDU_SECRET_READS=allow`.
|
|
215
239
|
That flag permits credential-resource operations; it never enables plaintext results. The
|
|
216
240
|
SDK masks credential fields and confidential or unclassified custom fields as `[REDACTED]`.
|
|
@@ -238,17 +262,39 @@ background, so a caller's later search is body-aware without the first one await
|
|
|
238
262
|
walk (some clients enforce a 60s call timeout). Under `stdio` the one long-lived index stays warm and the
|
|
239
263
|
default is `"index"`. Pass `tier:"index"` explicitly for a complete first answer.
|
|
240
264
|
|
|
241
|
-
|
|
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.
|
|
265
|
+
The read-through cache (SDK `recommended` preset), the 429 cooldown gate and the 30 s whole-call deadline are fixed, not env-tunable.
|
|
249
266
|
`blockPrivateHosts` deliberately gets no override: it is the literal-IP SSRF floor for
|
|
250
267
|
caller-named origins — a security property, not a deployment preference.
|
|
251
268
|
|
|
269
|
+
## Article bodies are Markdown (node-hudu 0.14+)
|
|
270
|
+
|
|
271
|
+
`hudu_read` and `hudu_write` pass `contentFormat: 'markdown'` to the SDK, and `hudu_get_article_context`
|
|
272
|
+
asks for Markdown, so single-article bodies (`articles.get`, `articles.getContext`, `articles.resolve`,
|
|
273
|
+
`create`, `update`) are returned and accepted as Markdown. Hudu callouts (`> [!INFO]`), accordions, task
|
|
274
|
+
lists and Mermaid fences (stored as the editor-native `<pre class="language-mermaid">` form) round-trip as the dialect documents. Rows from `hudu_fetch_many`, `hudu_read` `articles.list`, `articles.search` and `articles.findBySlug` still carry stored HTML:
|
|
275
|
+
those operations take no format. Rules a caller needs:
|
|
276
|
+
|
|
277
|
+
- A write's `data.content` is parsed as Markdown unless it opens with block-level HTML (`<p>`, `<div>`,
|
|
278
|
+
`<h1>`-`<h6>`, `<table>`, lists, `<pre>`, `<blockquote>`, `<hr>`, Hudu's `mce-accordion`), in which case
|
|
279
|
+
it is sent as HTML. Re-read an article through `articles.get` before editing it.
|
|
280
|
+
- A Markdown write is refused when the stored article holds something the dialect cannot express
|
|
281
|
+
(for example a `<video>`); send HTML to keep it. The SDK's loss guard runs on every write.
|
|
282
|
+
- 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).
|
|
283
|
+
- `articles.getSection` / `articles.patchSection` read or edit one section as Markdown, with dry-run.
|
|
284
|
+
`articles.getSection` carries no `markdown_lossy` signal (the SDK drops `content_loss` inside it, and a
|
|
285
|
+
section is not the whole body); read the whole article with `articles.get` to learn whether it is lossy.
|
|
286
|
+
- A Markdown read can be incomplete: anything the dialect cannot express is dropped or reduced
|
|
287
|
+
(`<video>` and `<script>` are dropped entirely). `hudu_read` (`articles.get` / `getContext` / `resolve`) and
|
|
288
|
+
`hudu_get_article_context` then set `markdown_lossy: true` (with `markdown_lossy_codes`, the SDK's
|
|
289
|
+
`diffArticleRoundTrip` content-loss codes) and add one sentence to the result text; fetch the stored
|
|
290
|
+
HTML with `hudu_fetch_many` (`{items:[{resource:"articles", id}]}`) to see the full body. The read is one
|
|
291
|
+
fetch: the SDK converts the body and reports the loss on the record (no second request); a caller's own
|
|
292
|
+
`opts.format: "html"` returns the stored HTML untouched with no flag. Callouts, accordions, task
|
|
293
|
+
lists, Mermaid, inline `<svg>` and `<iframe>` round-trip cleanly and are never flagged; a lossless read is unchanged.
|
|
294
|
+
A body that converts to nothing (only `<video>`, `<input>`, `<p></p>`) reads as `content: ""` with
|
|
295
|
+
`markdown_lossy_codes` containing `ROUNDTRIP_BODY_EMPTY` (it used to fail with `CONVERSION_EMPTY_OUTPUT`).
|
|
296
|
+
The SDK fails a Markdown read with `CONFIG_ERROR` when the Markdown form exceeds 256 KiB.
|
|
297
|
+
|
|
252
298
|
## SDK behaviour (0.12.0)
|
|
253
299
|
|
|
254
300
|
Three SDK behaviours changed in the 0.12.0 line and shape a deployment:
|
|
@@ -284,13 +330,13 @@ Every client the server builds — the parent, the fail-closed placeholders and
|
|
|
284
330
|
(1 retry, 240/min rate limit, concurrency 3, bounded resolution scans). The rate-limit bucket
|
|
285
331
|
is per client, i.e. per scope. The preset stays a deployment-wide constant with no env
|
|
286
332
|
override; the one deadline it would otherwise govern is pinned explicitly instead
|
|
287
|
-
(
|
|
333
|
+
(a fixed 30 s, below), and the 429 now arms the cooldown gate instead of retrying
|
|
288
334
|
blindly.
|
|
289
|
-
- `cache: 'recommended'` —
|
|
335
|
+
- `cache: 'recommended'` — fixed on: the
|
|
290
336
|
per-entity read-through TTL preset (1 h structural / 5 min mutable / 1 min base). Caches are
|
|
291
337
|
per client, so per scope. A write through the write or delete client does not clear the read
|
|
292
338
|
client's cache (separate instances), so a read after a mutation is TTL-bounded stale, never
|
|
293
|
-
unbounded.
|
|
339
|
+
unbounded.
|
|
294
340
|
- `blockPrivateHosts` on the per-scope clients: the literal-IP SSRF floor for CALLER-named
|
|
295
341
|
origins, fail-closed at client construction. It complements the `HUDU_ALLOWED_BASE_HOSTS`
|
|
296
342
|
hostname matcher (which alone can express wildcard suffixes) and holds even in
|
|
@@ -299,20 +345,17 @@ Every client the server builds — the parent, the fail-closed placeholders and
|
|
|
299
345
|
env boundary and is never floor-checked, so an on-prem install whose Hudu sits on a literal
|
|
300
346
|
private IP is unaffected by the upgrade. Deliberately no env override: it is a security
|
|
301
347
|
property, not a deployment preference.
|
|
302
|
-
- `cooldown: { enabled: true }` —
|
|
348
|
+
- `cooldown: { enabled: true }` — fixed on: the 429
|
|
303
349
|
cooldown gate; a call refused locally by the gate answers with `notSent: true` in the error
|
|
304
350
|
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
351
|
|
|
307
|
-
The whole-call deadline is
|
|
352
|
+
The whole-call deadline is a fixed `30000` ms:
|
|
308
353
|
it is always passed to the SDK explicitly, so it stays above the agent profile's 20 s
|
|
309
354
|
regardless of preset — the preset's 20 s would shorten a cold `tier:'index'` body-index walk
|
|
310
355
|
that the 0.9.2 deployment tolerated at the SDK's 30 s, and a cold-scope search must not start
|
|
311
356
|
timing out because of an upgrade.
|
|
312
357
|
|
|
313
|
-
These are deployment-wide, not per-caller
|
|
314
|
-
`HUDU_TIMEOUT_MS` are env-tunable with the defaults above, while `profile` and
|
|
315
|
-
`blockPrivateHosts` stay constants.
|
|
358
|
+
These are deployment-wide constants, not per-caller and not env-tunable.
|
|
316
359
|
|
|
317
360
|
|
|
318
361
|
## Development
|
|
@@ -320,16 +363,16 @@ These are deployment-wide, not per-caller: `HUDU_CACHE_PRESET`, `HUDU_COOLDOWN`
|
|
|
320
363
|
```
|
|
321
364
|
git clone https://github.com/msoukhomlinov/iit-mcp-hudu.git
|
|
322
365
|
cd iit-mcp-hudu
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
366
|
+
pnpm install
|
|
367
|
+
pnpm run build
|
|
368
|
+
pnpm test
|
|
369
|
+
pnpm run dev
|
|
327
370
|
```
|
|
328
371
|
|
|
329
372
|
CI (`.github/workflows/ci.yml`) runs `typecheck`, `lint` and `test` on every push and pull request.
|
|
330
373
|
|
|
331
|
-
`node-hudu` is pinned to `0.
|
|
332
|
-
review of the client authority, operation enums and redaction regressions. Docker uses `
|
|
374
|
+
`node-hudu` is pinned to `0.17.0`. The catalog hash test detects metadata drift; upgrades require
|
|
375
|
+
review of the client authority, operation enums and redaction regressions. Docker uses `pnpm install --frozen-lockfile`.
|
|
333
376
|
|
|
334
377
|
## Deployment
|
|
335
378
|
|
package/dist/auth.js
CHANGED
package/dist/base-hosts.js
CHANGED
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`
|
|
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
|
-
*
|
|
164
|
-
*
|
|
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
|
package/dist/credential.js
CHANGED
package/dist/doctor.js
CHANGED
package/dist/http.js
CHANGED
package/dist/lifecycle.js
CHANGED
package/dist/logger.js
CHANGED
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
|