iit-mcp-hudu 0.1.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 (54) hide show
  1. package/.env.example +81 -0
  2. package/LICENSE +21 -0
  3. package/README.md +233 -0
  4. package/dist/auth.d.ts +51 -0
  5. package/dist/auth.js +84 -0
  6. package/dist/base-hosts.d.ts +20 -0
  7. package/dist/base-hosts.js +39 -0
  8. package/dist/config.d.ts +86 -0
  9. package/dist/config.js +323 -0
  10. package/dist/http.d.ts +45 -0
  11. package/dist/http.js +129 -0
  12. package/dist/lifecycle.d.ts +29 -0
  13. package/dist/lifecycle.js +52 -0
  14. package/dist/logger.d.ts +24 -0
  15. package/dist/logger.js +26 -0
  16. package/dist/main.d.ts +2 -0
  17. package/dist/main.js +56 -0
  18. package/dist/policy.d.ts +79 -0
  19. package/dist/policy.js +175 -0
  20. package/dist/stdio.d.ts +21 -0
  21. package/dist/stdio.js +29 -0
  22. package/dist/tools/company-context-policy.d.ts +16 -0
  23. package/dist/tools/company-context-policy.js +59 -0
  24. package/dist/tools/context.d.ts +31 -0
  25. package/dist/tools/context.js +2 -0
  26. package/dist/tools/dedicated.d.ts +110 -0
  27. package/dist/tools/dedicated.js +227 -0
  28. package/dist/tools/dispatch.d.ts +4 -0
  29. package/dist/tools/dispatch.js +115 -0
  30. package/dist/tools/lists.d.ts +49 -0
  31. package/dist/tools/lists.js +265 -0
  32. package/dist/tools/meta.d.ts +7 -0
  33. package/dist/tools/meta.js +73 -0
  34. package/dist/tools/reads.d.ts +7 -0
  35. package/dist/tools/reads.js +277 -0
  36. package/dist/tools/register.d.ts +12 -0
  37. package/dist/tools/register.js +55 -0
  38. package/dist/tools/result-urls.d.ts +2 -0
  39. package/dist/tools/result-urls.js +47 -0
  40. package/dist/tools/results.d.ts +97 -0
  41. package/dist/tools/results.js +155 -0
  42. package/dist/tools/schemas.d.ts +119 -0
  43. package/dist/tools/schemas.js +142 -0
  44. package/dist/tools/search-policy.d.ts +124 -0
  45. package/dist/tools/search-policy.js +236 -0
  46. package/dist/tools/search-prose.d.ts +79 -0
  47. package/dist/tools/search-prose.js +327 -0
  48. package/dist/tools/search.d.ts +6 -0
  49. package/dist/tools/search.js +173 -0
  50. package/dist/tools/server.d.ts +77 -0
  51. package/dist/tools/server.js +319 -0
  52. package/dist/tools.d.ts +34 -0
  53. package/dist/tools.js +34 -0
  54. package/package.json +59 -0
package/.env.example ADDED
@@ -0,0 +1,81 @@
1
+ # The Hudu origin the server talks to -- REQUIRED for stdio, an optional default for http.
2
+ #
3
+ # stdio: the process IS the trust boundary, so the origin comes from the environment.
4
+ # http: callers normally name their own origin per request in the X-Hudu-Base-Url header. When set,
5
+ # this is the origin used by a request that sends no such header. The default key below is
6
+ # only ever presented to this origin.
7
+ # HUDU_BASE_URL=https://hudu.example.com
8
+
9
+ # stdio (local: Claude Code, Claude Desktop, VS Code) or http (remote MCP clients).
10
+ MCP_TRANSPORT=stdio
11
+
12
+ # The server-held Hudu credential -- REQUIRED for stdio, an optional default for http.
13
+ #
14
+ # stdio: the process IS the trust boundary, so the key comes from the environment.
15
+ # http: the key normally arrives per request in the X-Hudu-Api-Key header (a client can scope it
16
+ # per user or per agent, so each caller reaches Hudu as itself). When set here, every request
17
+ # WITHOUT its own key runs as this key, so set it only when that is intended. It is only ever
18
+ # sent to HUDU_BASE_URL, never to a caller-named origin. Left commented so a copied example
19
+ # does not set an empty value.
20
+ # HUDU_API_KEY=
21
+
22
+ # http only, optional. Hosts a caller may name in X-Hudu-Base-Url: comma-separated exact hostnames or
23
+ # *.example.com subdomain suffixes (no scheme, port or path). Unset alongside a set HUDU_BASE_URL,
24
+ # only that default origin is accepted. Unset with no HUDU_BASE_URL, any caller-named origin would be
25
+ # accepted, so whoever can reach the listener could make the server dial hosts of their choosing:
26
+ # the server refuses to boot in that case unless HUDU_ALLOW_ANY_BASE_HOST=true. An explicit list
27
+ # never checks HUDU_BASE_URL against it.
28
+ # HUDU_ALLOWED_BASE_HOSTS=hudu.example.com,*.corp.example.com
29
+
30
+ # http only. Boot without an allow-list and accept any origin a caller names. Use only when every
31
+ # caller that can reach the listener is trusted.
32
+ # HUDU_ALLOW_ANY_BASE_HOST=false
33
+
34
+ # Omit mutation tools entirely by default. Set false before enabling a write policy.
35
+ HUDU_READ_ONLY=true
36
+
37
+ # Authority over the mutation dispatchers. deny (the default) refuses every write and destructive
38
+ # operation; allow_list permits only the keys in HUDU_WRITE_ALLOW; all is today's behaviour and has
39
+ # to be typed out deliberately. An unset value means deny, so forgetting this gets a read-only
40
+ # server, never an open one. Reads are never affected.
41
+ HUDU_WRITE_POLICY=deny
42
+
43
+ # Authority over reads that return a plaintext credential -- independent of HUDU_WRITE_POLICY, which
44
+ # never covers them: asset_passwords.get returns password and otp_secret as an ordinary read. deny
45
+ # (the default) refuses every asset_passwords.* and password_folders.* operation, through the dispatchers
46
+ # and through hudu_find_asset_passwords_by_slug / hudu_get_password_folder alike. Scoped to the whole
47
+ # resource, not to the expand flag, because the secret comes back without expand. hudu_search is
48
+ # unaffected; it redacts those resources already.
49
+ HUDU_SECRET_READS=deny
50
+
51
+ # Only read under HUDU_WRITE_POLICY=allow_list. Comma-separated registry operation keys, e.g.
52
+ # articles.update,assets.create. It must be non-empty and every key must exist in the registry (and
53
+ # be a write) or the container fails at boot with the variable named. Ignored under deny/all.
54
+ # HUDU_WRITE_ALLOW=articles.update,assets.create
55
+
56
+ PORT=8787
57
+ LOG_LEVEL=info
58
+
59
+ # Pages the search body index walks per resource (SDK default 250 = 25,000 records). Lower it when
60
+ # the tenant's assets are numerous enough to evict article bodies from the 64 MB text budget: the
61
+ # article walk must fit with room to spare or every search answers body-blind. 40 covers ~4,000
62
+ # articles and caps assets at ~4,000.
63
+ HUDU_SEARCH_MAX_INDEX_PAGES=40
64
+
65
+ # The resources hudu_search and hudu_resolve_any may offer and query. Unset means the SDK's full
66
+ # searchable set (articles, asset_passwords, assets, companies, groups, password_folders, users,
67
+ # websites). Set it to the subset THIS deployment's Hudu API key can read, so a tool neither
68
+ # advertises a guaranteed-empty read nor leaks an authorization failure for a resource the key
69
+ # cannot reach. A Hudu API key inherits its user's role, so a "Reader" key may read fewer than all
70
+ # eight. Comma-separated, each name validated against the SDK's searchable set at boot. Applies to
71
+ # the cross-resource reads (hudu_search, hudu_resolve_any) and the same operations through
72
+ # hudu_read; the dedicated per-resource resolve tools are unaffected.
73
+ # HUDU_SEARCH_RESOURCES=articles,assets,companies,users,groups,websites
74
+
75
+ # Image tag docker-compose.yml builds and runs as. scripts/deploy.sh sets it to the git short SHA
76
+ # of the deployed commit, so the running container is always tied to a commit and the previous
77
+ # tag stays resolvable for rollback. Do not deploy by hand with it unset, and never tag :latest.
78
+ # Leave it out of .env — the deploy script exports it.
79
+ # HUDU_IMAGE_TAG=
80
+
81
+ # HUDU_SECRET_READS=allow permits resource access; SDK results remain redacted.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Max Soukhomlinov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # iit-mcp-hudu
2
+
3
+ MCP server for Hudu IT documentation, built on the [`node-hudu`](https://www.npmjs.com/package/node-hudu) SDK.
4
+
5
+ ## Tool surface
6
+
7
+ 19 tools by default: `hudu_list_operations`, `hudu_describe_operation`, `hudu_read`, twelve
8
+ core reads, and four bounded list reads. Setting `HUDU_READ_ONLY=false` adds `hudu_write` and
9
+ `hudu_delete` (21 tools). Each dispatcher uses a separate SDK client in its matching mode,
10
+ with names, annotations and operation enums from the SDK's curated metadata. SDK enforcement
11
+ rejects cross-effect calls before requests. The old `hudu_invoke`, `hudu_catalog` and
12
+ `hudu_describe` names are removed; consumers must resync their tool catalog.
13
+
14
+ The four list tools each wrap exactly ONE fixed registry operation (`companies.list` /
15
+ `articles.list` / `assets.listAcrossCompanies` / `asset_layouts.list`), take exactly one page and
16
+ accept no operation selector or nested invocation, so an ungated Reader can page companies,
17
+ articles, assets or asset layouts without using a registry dispatcher and without exposing the write
18
+ surface. The first three take `page` / `page_size` (capped at 100); `hudu_list_asset_layouts` takes
19
+ `page` only, because `/asset_layouts` ignores `page_size` — a caller bound would truncate a vendor
20
+ page and skip rows as `page` advanced, so the tool returns the vendor page whole and page N is
21
+ always the vendor's Nth page. For the searchable resources (`companies` / `articles` /
22
+ `assets`) a resource excluded by `HUDU_SEARCH_RESOURCES` is refused by the list tool too, before any
23
+ request; `asset_layouts` is not searchable (the SDK declares no vendor text filter for it), so
24
+ `hudu_list_asset_layouts` follows the same reachability as the existing ungated
25
+ `hudu_get_asset_layout` tool.
26
+
27
+ ## Transports
28
+
29
+ | Transport | For | Caller auth | Hudu credential |
30
+ |---|---|---|---|
31
+ | `stdio` | Claude Code, Claude Desktop, VS Code | none — the process boundary is the trust boundary | `HUDU_API_KEY` + `HUDU_BASE_URL` env |
32
+ | `http` | Remote MCP clients | none — private, unpublished network | per request: `X-Hudu-Api-Key` + `X-Hudu-Base-Url` headers, or the optional `HUDU_API_KEY` + `HUDU_BASE_URL` defaults |
33
+
34
+ ## Local use (stdio)
35
+
36
+ Needs Node 24+, a Hudu API key and your Hudu origin. `stdio` is the default transport, so no
37
+ `MCP_TRANSPORT` is needed. Mutation tools stay off unless you set `HUDU_READ_ONLY=false`.
38
+
39
+ **From a clone:**
40
+
41
+ ```
42
+ git clone https://github.com/msoukhomlinov/iit-mcp-hudu.git
43
+ cd iit-mcp-hudu
44
+ npm install
45
+ npm run build
46
+ ```
47
+
48
+ Then register `dist/main.js` with your client, e.g. Claude Code:
49
+
50
+ ```
51
+ claude mcp add hudu \
52
+ --env HUDU_API_KEY=your-key --env HUDU_BASE_URL=https://your.hudu.host \
53
+ -- node /absolute/path/to/iit-mcp-hudu/dist/main.js
54
+ ```
55
+
56
+ or in `.mcp.json` / Claude Desktop config:
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "hudu": {
62
+ "command": "node",
63
+ "args": ["/absolute/path/to/iit-mcp-hudu/dist/main.js"],
64
+ "env": { "HUDU_API_KEY": "your-key", "HUDU_BASE_URL": "https://your.hudu.host" }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ **From npm:**
71
+
72
+ ```
73
+ claude mcp add hudu \
74
+ --env HUDU_API_KEY=your-key --env HUDU_BASE_URL=https://your.hudu.host \
75
+ -- npx -y iit-mcp-hudu
76
+ ```
77
+
78
+ or `"command": "npx", "args": ["-y", "iit-mcp-hudu"]` in the JSON config above.
79
+
80
+ ## Configuration
81
+
82
+ Copy `.env.example`. Every variable is validated at boot; the process exits non-zero on an invalid
83
+ config rather than failing at the first tool call.
84
+
85
+ Under `http` the caller normally supplies both per request: its own Hudu API key in `X-Hudu-Api-Key`
86
+ and the origin that key belongs to in `X-Hudu-Base-Url` (an absolute `http`/`https` origin, no
87
+ path — validated by the SDK when the scoped client is built). A client can scope the key
88
+ per user or per agent to match how the connection is configured, so a caller can read exactly
89
+ what its own Hudu role permits and no more. There is no shared token: the listener is
90
+ unauthenticated and must stay on a private, unpublished network.
91
+
92
+ Both may instead be set as **defaults**. `HUDU_BASE_URL` and `HUDU_API_KEY` are required under
93
+ `stdio` and optional under `http`, where a request's headers win and the defaults fill in whatever
94
+ it omits. Setting `HUDU_API_KEY` under `http` means every request without its own key runs as that
95
+ key, so set it only when that is intended: with no shared token, the private network is the only
96
+ gate on who can use it. It is only ever presented to the default
97
+ `HUDU_BASE_URL`: a request that names some other origin and brings no key of its own gets no
98
+ credential and fails closed, so a caller cannot aim the server's key at a host it controls. (A
99
+ blank value, as a copied `.env.example` leaves it, is treated as unset.) Each (origin, credential)
100
+ pair gets its own scoped client (its own search index) cached by the pair's hash and capped, so
101
+ one caller's indexed article bodies can never answer another's search.
102
+
103
+ `HUDU_ALLOWED_BASE_HOSTS` (`http` only; required unless `HUDU_ALLOW_ANY_BASE_HOST=true`) restricts which origins a caller may name in
104
+ `X-Hudu-Base-Url`: a comma-separated list of exact hostnames or `*.example.com` subdomain suffixes
105
+ (no scheme, port or path; write an internationalised name in punycode). When it is unset and
106
+ `HUDU_BASE_URL` is set, only that default origin is accepted. When both are unset, any caller-named
107
+ origin would be accepted, so anyone who can reach the listener could make the server send requests
108
+ (carrying their key) to hosts of their choosing; the server refuses to boot in that case unless
109
+ `HUDU_ALLOW_ANY_BASE_HOST=true` opts in, for a listener only trusted callers can reach. The check is
110
+ on the hostname string: it does not resolve DNS or block private addresses, so do not list a name you
111
+ do not control. A request naming a host outside the effective policy fails closed on every tool call
112
+ without dialing it. Only the hostname is checked for an explicit list: an allowed host is accepted on
113
+ any port and over `http`. The default `HUDU_BASE_URL` is never checked against an explicit list.
114
+
115
+ With no usable key (none in the header and no `HUDU_API_KEY` default) a request is still let
116
+ through: `initialize` and `tools/list` never reach Hudu, and a client's catalog sync needs them. Such
117
+ a request resolves to a credential-less client that throws before any Hudu request is built, so
118
+ every tool call fails closed rather than borrowing an identity. A request that has a key but no
119
+ origin (no header and no `HUDU_BASE_URL` default) is let through the same way and fails closed on
120
+ every tool call, with the error naming the missing header. A
121
+ credential header sent twice (which Node would otherwise join with `", "`) is treated as absent
122
+ for the same reason.
123
+
124
+ `HUDU_READ_ONLY=true` is the default and omits both mutation tools entirely, regardless of
125
+ write policy. To enable mutations, explicitly set it to `false` and configure
126
+ `HUDU_WRITE_POLICY`: `deny` rejects all mutations, `allow_list` permits only the canonical
127
+ keys in `HUDU_WRITE_ALLOW`, and `all` permits the SDK's mutation surface. An empty or invalid
128
+ allow-list fails startup. The SDK clients receive the same allow-list plus reads needed by
129
+ helper dependencies; client mode and effect dispatch remain independent restrictions.
130
+ Writes default to dry-run previews; execution requires `dry_run:false`, and destructive
131
+ operations additionally require exact `confirm`. A confirmation string is not human consent.
132
+ Discovery reports deployment-policy refusals as unreachable.
133
+
134
+ `HUDU_WRITE_POLICY` stays a **deployment-wide** decision, not a per-caller one. Per-caller
135
+ credentials make each caller's Hudu role an external authority too, but that authority is not ours
136
+ to trust and cannot be allowed to widen this server's surface: the server-side policy is the floor,
137
+ `deny` by default, and it holds even when a caller presents a key whose Hudu role could write. The
138
+ same applies to `HUDU_SECRET_READS` and `HUDU_SEARCH_RESOURCES` — all three narrow what *this
139
+ server* will ever offer, independent of what any one caller's key might otherwise permit.
140
+
141
+ `HUDU_SECRET_READS` governs the other thing no write policy covers: reads that return a plaintext
142
+ credential. `asset_passwords.get` returns `password` and `otp_secret` as an ordinary read, and the
143
+ curated tools `hudu_find_asset_passwords_by_slug` and `hudu_get_password_folder` reach the same
144
+ records. It is `deny` by default and refuses every operation on the `asset_passwords` and
145
+ `password_folders` resources — through `hudu_read` and through those two tools alike, before any
146
+ request is issued — so a deployment cannot hand a secret to a model by omission. It is scoped to the
147
+ whole resource rather than to the `expand` flag, because `asset_passwords.get` returns the secret
148
+ without `expand`. It is independent of `HUDU_WRITE_POLICY`: `all` does not imply open secrets, and
149
+ `allow` does not open writes. `hudu_search` is unaffected — it already redacts hits on those two
150
+ resources. Refusals are `POLICY_DENIED` and appear as unreachable rows in `hudu_list_operations`.
151
+
152
+ All SDK clients keep default redaction enabled, including when `HUDU_SECRET_READS=allow`.
153
+ That flag permits credential-resource operations; it never enables plaintext results. The
154
+ SDK masks credential fields and confidential or unclassified custom fields as `[REDACTED]`.
155
+ Untyped fields may therefore be masked even when an external layout calls them public.
156
+ Under `deny`, both company-context paths additionally project password rows to the summary
157
+ field allow-list, dropping free-text descriptions and unknown future fields. No extra layout
158
+ lookups are needed. The common result
159
+ boundary also uses the SDK redactor with `otp_seed`, `recovery_code`, and `recovery_codes`
160
+ as additional secret keys, preserving protections absent from the SDK's default key list.
161
+
162
+ Result text is a JSON envelope labelled `untrusted_data`; its fixed instruction marks both
163
+ text and `structuredContent` as data, never instructions or authorization to invoke tools.
164
+
165
+ `HUDU_SEARCH_RESOURCES` is the read-edge counterpart: it narrows which resources the cross-resource
166
+ reads — `hudu_search` and `hudu_resolve_any`, plus the same operations reached through `hudu_read`
167
+ — may offer and query, because a Hudu API key inherits its user's role and may read fewer than the
168
+ SDK's eight searchable resources. Unset means the full set; set it to what the credential can read so
169
+ the schema never advertises a guaranteed-empty read. A per-resource read failure, and an auth failure
170
+ thrown by the resolve fan-out, are reported as unavailable — never with Hudu's own authorization
171
+ message, which is a fact about the calling credential, not the caller's query.
172
+
173
+ Under `http` each caller's search scope starts cold, so `hudu_search` defaults to `tier:"auto"`
174
+ rather than `"index"`: it answers from Hudu search immediately and builds the body index in the
175
+ background, so a caller's later search is body-aware without the first one awaiting a corpus-wide
176
+ walk (some clients enforce a 60s call timeout). Under `stdio` the one long-lived index stays warm and the
177
+ default is `"index"`. Pass `tier:"index"` explicitly for a complete first answer.
178
+
179
+ ## Development
180
+
181
+ ```
182
+ npm install
183
+ npm test
184
+ npm run dev
185
+ ```
186
+
187
+ CI (`.github/workflows/ci.yml`) runs `typecheck`, `lint` and `test` on every push and pull request.
188
+
189
+ `node-hudu` is pinned to `0.9.2`. The catalog hash test detects metadata drift; upgrades require
190
+ review of the client authority, operation enums and redaction regressions. Docker uses `npm ci`.
191
+
192
+ ## Deployment
193
+
194
+ `Dockerfile` builds a non-root runtime image. The healthcheck is liveness-only and deliberately does
195
+ not call Hudu — otherwise an upstream outage would cycle the container, turning a partial failure
196
+ into a total one.
197
+
198
+ The deployment definition is `docker-compose.yml`: its own Compose project, joining an existing
199
+ `app_net` external network (create it with `docker network create app_net`, or rename it in the
200
+ file). The listener is not published to the host, so put a TLS-terminating reverse proxy in front of
201
+ it, or share the network with the client that calls it. Copy `.env.example` to `.env` and fill it
202
+ in, then deploy from a clean git checkout with:
203
+
204
+ scripts/deploy.sh
205
+
206
+ The script tags the build with the git short SHA of HEAD (`iit-mcp-hudu-iit-mcp-hudu:<sha>`) —
207
+ never `:latest` — so the running container is always tied to a commit.
208
+
209
+ ### Rolling back
210
+
211
+ A rebuild cannot touch a previous tag, so it stays resolvable in the local image store. Roll back
212
+ from the same checkout — and only from it:
213
+
214
+ docker image inspect iit-mcp-hudu-iit-mcp-hudu:<previous-sha> >/dev/null # must exist locally
215
+ HUDU_IMAGE_TAG=<previous-sha> docker compose up -d --no-build --pull never
216
+
217
+ `--no-build` and `--pull never` are not optional: the service declares both `image` and `build`,
218
+ so without them Compose would fall back to pulling — or building the current source under the old
219
+ tag — if the image is missing, a rollback that deploys the new code instead. The compose file
220
+ also pins `pull_policy: never` as the backstop for any invocation, and the `docker image inspect`
221
+ line makes a pruned or absent image fail loudly before anything is started.
222
+
223
+ The image that was running before the first tagged deploy was `:latest` (unpinned); until a newer
224
+ pinned tag exists it remains the rollback target via the same two commands with `latest`.
225
+
226
+ ## Release checklist
227
+
228
+ - Server identity (`name` / `version` reported in `initialize`) is hardcoded in `src/tools/server.ts`,
229
+ because `rootDir: "src"` forbids importing `package.json` from across the repo root. Bump it there
230
+ as well as in `package.json`.
231
+ - `BUILT_AGAINST_PLAN_HASH` in `src/tools.ts` pins the SDK catalogue this server was built against.
232
+ If its test fails after an SDK upgrade, re-read the catalogue and re-check the 19 registered tools,
233
+ then move the constant. Never move it to make CI green — that is the one thing it exists to stop.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * auth.ts — caller authentication for the HTTP transport.
3
+ *
4
+ * Two per-request headers answer different questions:
5
+ *
6
+ * - `X-Hudu-Api-Key` is the caller's own Hudu key. Under the `http` transport the server normally
7
+ * holds no Hudu credential of its own (an operator may set an optional `HUDU_API_KEY` default for
8
+ * requests that bring none): the client supplies the key per request,
9
+ * scoped per user or per agent to match how the connection is configured. That key is
10
+ * what actually reaches Hudu, so a caller can read exactly what its own Hudu role permits and no
11
+ * more.
12
+ * - `X-Hudu-Base-Url` is the origin that key belongs to. It is not a secret — the caller supplied
13
+ * it and only ever sees its own value — but it decides which Hudu tenant the key is presented
14
+ * to, so it is scoped exactly like the key: per request, single-valued, absent when duplicated,
15
+ * and never in the server's config surface or tool schemas.
16
+ *
17
+ * The legacy `X-MCP-Token` shared-secret gate is gone (see the 2026-09-29 amendment to the design
18
+ * spec): the transport boundary is the private, unpublished network the listener must stay on. A
19
+ * client that still sends `x-mcp-token` is answered exactly as if the header were not there.
20
+ *
21
+ * Neither header ever appears in a tool's input schema. See design spec §5.4.
22
+ */
23
+ /**
24
+ * The header carrying the caller's Hudu API key.
25
+ *
26
+ * Fixed rather than configurable: it is the wire contract clients are built to send, and making it a
27
+ * knob would let a deployment and its only consumer disagree silently. The value is per request, so
28
+ * it never enters the server's config surface, logs, or tool schemas.
29
+ */
30
+ export declare const HUDU_KEY_HEADER = "x-hudu-api-key";
31
+ /**
32
+ * The header carrying the caller's Hudu base URL — the origin its key belongs to.
33
+ *
34
+ * Sibling of {@link HUDU_KEY_HEADER}: fixed rather than configurable, per request, and never in
35
+ * the server's config surface or tool schemas. It is not a secret — the caller supplied it and
36
+ * only ever sees its own value — so it is not bound by the key's no-logging rule; the
37
+ * single-valued rule still applies, though: a base URL presented twice is absent, exactly as with
38
+ * the key.
39
+ */
40
+ export declare const HUDU_BASE_URL_HEADER = "x-hudu-base-url";
41
+ /** Read the caller's per-request Hudu API key from request headers. */
42
+ export declare function readHuduKey(headers: Record<string, string | string[] | undefined>): string | undefined;
43
+ /** Read the caller's per-request Hudu base URL from request headers. */
44
+ export declare function readHuduBase(headers: Record<string, string | string[] | undefined>): string | undefined;
45
+ /**
46
+ * Collapse a web `Headers` bag into the lowercased record shape the readers expect.
47
+ *
48
+ * A header given twice becomes an array, which the readers treat as absent — see
49
+ * {@link readSingleHeader}. That is the whole reason this cannot just be `Object.fromEntries`.
50
+ */
51
+ export declare function headerRecord(headers: Headers): Record<string, string | string[] | undefined>;
package/dist/auth.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * auth.ts — caller authentication for the HTTP transport.
3
+ *
4
+ * Two per-request headers answer different questions:
5
+ *
6
+ * - `X-Hudu-Api-Key` is the caller's own Hudu key. Under the `http` transport the server normally
7
+ * holds no Hudu credential of its own (an operator may set an optional `HUDU_API_KEY` default for
8
+ * requests that bring none): the client supplies the key per request,
9
+ * scoped per user or per agent to match how the connection is configured. That key is
10
+ * what actually reaches Hudu, so a caller can read exactly what its own Hudu role permits and no
11
+ * more.
12
+ * - `X-Hudu-Base-Url` is the origin that key belongs to. It is not a secret — the caller supplied
13
+ * it and only ever sees its own value — but it decides which Hudu tenant the key is presented
14
+ * to, so it is scoped exactly like the key: per request, single-valued, absent when duplicated,
15
+ * and never in the server's config surface or tool schemas.
16
+ *
17
+ * The legacy `X-MCP-Token` shared-secret gate is gone (see the 2026-09-29 amendment to the design
18
+ * spec): the transport boundary is the private, unpublished network the listener must stay on. A
19
+ * client that still sends `x-mcp-token` is answered exactly as if the header were not there.
20
+ *
21
+ * Neither header ever appears in a tool's input schema. See design spec §5.4.
22
+ */
23
+ /**
24
+ * The header carrying the caller's Hudu API key.
25
+ *
26
+ * Fixed rather than configurable: it is the wire contract clients are built to send, and making it a
27
+ * knob would let a deployment and its only consumer disagree silently. The value is per request, so
28
+ * it never enters the server's config surface, logs, or tool schemas.
29
+ */
30
+ export const HUDU_KEY_HEADER = 'x-hudu-api-key';
31
+ /**
32
+ * The header carrying the caller's Hudu base URL — the origin its key belongs to.
33
+ *
34
+ * Sibling of {@link HUDU_KEY_HEADER}: fixed rather than configurable, per request, and never in
35
+ * the server's config surface or tool schemas. It is not a secret — the caller supplied it and
36
+ * only ever sees its own value — so it is not bound by the key's no-logging rule; the
37
+ * single-valued rule still applies, though: a base URL presented twice is absent, exactly as with
38
+ * the key.
39
+ */
40
+ export const HUDU_BASE_URL_HEADER = 'x-hudu-base-url';
41
+ /**
42
+ * Read a single-valued header, tolerating Node's array-valued header shape.
43
+ *
44
+ * A duplicated header is treated as absent rather than trusting the first value: a request that
45
+ * presents two different credentials is malformed, and picking one is how request-smuggling bugs
46
+ * start.
47
+ */
48
+ function readSingleHeader(headers, name) {
49
+ const raw = headers[name];
50
+ if (typeof raw === 'string')
51
+ return raw;
52
+ if (Array.isArray(raw) && raw.length === 1)
53
+ return raw[0];
54
+ return undefined;
55
+ }
56
+ /** Read the caller's per-request Hudu API key from request headers. */
57
+ export function readHuduKey(headers) {
58
+ return readSingleHeader(headers, HUDU_KEY_HEADER);
59
+ }
60
+ /** Read the caller's per-request Hudu base URL from request headers. */
61
+ export function readHuduBase(headers) {
62
+ return readSingleHeader(headers, HUDU_BASE_URL_HEADER);
63
+ }
64
+ /**
65
+ * Collapse a web `Headers` bag into the lowercased record shape the readers expect.
66
+ *
67
+ * A header given twice becomes an array, which the readers treat as absent — see
68
+ * {@link readSingleHeader}. That is the whole reason this cannot just be `Object.fromEntries`.
69
+ */
70
+ export function headerRecord(headers) {
71
+ const out = {};
72
+ headers.forEach((value, name) => {
73
+ const key = name.toLowerCase();
74
+ const existing = out[key];
75
+ if (existing === undefined)
76
+ out[key] = value;
77
+ else if (Array.isArray(existing))
78
+ existing.push(value);
79
+ else
80
+ out[key] = [existing, value];
81
+ });
82
+ return out;
83
+ }
84
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * base-hosts.ts — the optional allow-list for client-supplied Hudu origins.
3
+ *
4
+ * Under `http` a caller names the origin its key is presented to (`x-hudu-base-url`), which makes
5
+ * this server an outbound HTTP client for whoever can reach the listener. The SDK only checks the
6
+ * scheme and that there is no path, so when `HUDU_ALLOWED_BASE_HOSTS` is set this is what keeps a
7
+ * caller from pointing the server at an internal host or a hostile one.
8
+ */
9
+ /**
10
+ * Whether `baseUrl`'s host is permitted by `allowed`.
11
+ *
12
+ * An entry is an exact hostname (`hudu.example.com`) or a `*.` suffix (`*.example.com`) that
13
+ * matches subdomains only, never the bare domain. Unset (`undefined`) permits everything at this
14
+ * level; the http server layers the effective policy on top (default-only when a default origin is
15
+ * set, bring-your-own-origin when it is not). A URL that does not parse is refused rather than
16
+ * passed through, so the SDK's own error is never the gate.
17
+ */
18
+ export declare function baseHostAllowed(baseUrl: string, allowed: readonly string[] | undefined): boolean;
19
+ /** The origin of `url`, or `undefined` when it does not parse. Used to compare against the default. */
20
+ export declare function originOf(url: string): string | undefined;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * base-hosts.ts — the optional allow-list for client-supplied Hudu origins.
3
+ *
4
+ * Under `http` a caller names the origin its key is presented to (`x-hudu-base-url`), which makes
5
+ * this server an outbound HTTP client for whoever can reach the listener. The SDK only checks the
6
+ * scheme and that there is no path, so when `HUDU_ALLOWED_BASE_HOSTS` is set this is what keeps a
7
+ * caller from pointing the server at an internal host or a hostile one.
8
+ */
9
+ /**
10
+ * Whether `baseUrl`'s host is permitted by `allowed`.
11
+ *
12
+ * An entry is an exact hostname (`hudu.example.com`) or a `*.` suffix (`*.example.com`) that
13
+ * matches subdomains only, never the bare domain. Unset (`undefined`) permits everything at this
14
+ * level; the http server layers the effective policy on top (default-only when a default origin is
15
+ * set, bring-your-own-origin when it is not). A URL that does not parse is refused rather than
16
+ * passed through, so the SDK's own error is never the gate.
17
+ */
18
+ export function baseHostAllowed(baseUrl, allowed) {
19
+ if (allowed === undefined)
20
+ return true;
21
+ let host;
22
+ try {
23
+ host = new URL(baseUrl).hostname.toLowerCase();
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ return allowed.some((entry) => entry.startsWith('*.') ? host.endsWith(entry.slice(1)) : host === entry);
29
+ }
30
+ /** The origin of `url`, or `undefined` when it does not parse. Used to compare against the default. */
31
+ export function originOf(url) {
32
+ try {
33
+ return new URL(url).origin;
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ //# sourceMappingURL=base-hosts.js.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * config.ts — the process's entire configuration surface, validated once at boot.
3
+ *
4
+ * Every knob is an environment variable. The server refuses to start on an invalid config rather
5
+ * than failing at the first tool call: a missing credential discovered by a model mid-conversation
6
+ * is a far worse failure than a container that never came up.
7
+ *
8
+ */
9
+ import * as z from 'zod';
10
+ /**
11
+ * `stdio` is the local path (Claude Code, Claude Desktop, VS Code): one process, one credential,
12
+ * the process boundary IS the trust boundary. `http` is the remote path (an HTTP MCP client)
13
+ * and takes the caller's Hudu credential per request.
14
+ */
15
+ export declare const TRANSPORTS: readonly ["stdio", "http"];
16
+ export type Transport = (typeof TRANSPORTS)[number];
17
+ /**
18
+ * The deployment's authority over `hudu_read / hudu_write / hudu_delete`'s write surface.
19
+ *
20
+ * `deny` is the default on purpose: an operator who forgets the variable gets a read-only server,
21
+ * never an open one. `all` is today's behaviour and has to be typed out deliberately.
22
+ */
23
+ export declare const WRITE_POLICIES: readonly ["deny", "allow_list", "all"];
24
+ export type WritePolicy = (typeof WRITE_POLICIES)[number];
25
+ /**
26
+ * The deployment's authority over reads that can return a plaintext credential.
27
+ *
28
+ * `asset_passwords.get` returns `password` and `otp_secret` in plaintext as an ordinary read, so no
29
+ * write policy touches it — so it needs its own flag that closes both reachable paths in one place. `deny` is the default for the same reason
30
+ * {@link WRITE_POLICIES} defaults that way: an operator who forgets the variable gets a server that
31
+ * cannot hand a secret to a model, never one that can.
32
+ */
33
+ export declare const SECRET_READ_POLICIES: readonly ["deny", "allow"];
34
+ export type SecretReadPolicy = (typeof SECRET_READ_POLICIES)[number];
35
+ declare const schema: z.ZodObject<{
36
+ HUDU_BASE_URL: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
37
+ HUDU_API_KEY: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
38
+ HUDU_ALLOWED_BASE_HOSTS: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string[] | undefined, string | undefined>>;
39
+ HUDU_ALLOW_ANY_BASE_HOST: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
40
+ true: "true";
41
+ false: "false";
42
+ }>>, z.ZodTransform<boolean, "true" | "false">>;
43
+ MCP_TRANSPORT: z.ZodDefault<z.ZodEnum<{
44
+ stdio: "stdio";
45
+ http: "http";
46
+ }>>;
47
+ HUDU_SEARCH_MAX_INDEX_PAGES: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
48
+ HUDU_SEARCH_RESOURCES: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string[] | undefined, string | undefined>>;
49
+ HUDU_READ_ONLY: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
50
+ true: "true";
51
+ false: "false";
52
+ }>>, z.ZodTransform<boolean, "true" | "false">>;
53
+ HUDU_WRITE_POLICY: z.ZodDefault<z.ZodEnum<{
54
+ deny: "deny";
55
+ allow_list: "allow_list";
56
+ all: "all";
57
+ }>>;
58
+ HUDU_WRITE_ALLOW: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string[], string | undefined>>;
59
+ HUDU_SECRET_READS: z.ZodDefault<z.ZodEnum<{
60
+ deny: "deny";
61
+ allow: "allow";
62
+ }>>;
63
+ PORT: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
64
+ LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
65
+ debug: "debug";
66
+ info: "info";
67
+ warn: "warn";
68
+ error: "error";
69
+ }>>;
70
+ }, z.core.$strip>;
71
+ export type Config = z.infer<typeof schema>;
72
+ /**
73
+ * Parse and validate configuration from an environment-shaped record.
74
+ *
75
+ * Takes the environment as an argument rather than reading `process.env` directly so the failure
76
+ * paths are testable without mutating global state.
77
+ *
78
+ * @throws {ConfigError} when any variable is missing or malformed. The message lists every problem
79
+ * at once — a boot loop that surfaces one missing variable per restart wastes real minutes.
80
+ */
81
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
82
+ /** Thrown only by {@link loadConfig}. Never carries a credential value — only variable names. */
83
+ export declare class ConfigError extends Error {
84
+ readonly name = "ConfigError";
85
+ }
86
+ export {};