iit-mcp-hudu 0.8.0 → 0.9.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.1
4
+
5
+ **Upgrading from 0.9.0:** no configuration change. This release sharpens the argument prose a calling
6
+ model reads without asking for help, and refuses arguments `hudu_search` does not implement.
7
+
8
+ - Change: `hudu_search`'s `company_id` description now states the filter's real semantics — a
9
+ **numeric** id only (a company name cannot be filtered: the Typesense tier sends the number into
10
+ `filter_by` and the vendor path compares the number), and omission means all companies **plus
11
+ global articles**, which is the wide answer rather than a narrow one. `resources` now says what the
12
+ list means ("only these resource types") and that fewer collections is less work. Funded by trimming
13
+ redundant prose in `mode` / `updated_since` / `min_score` / `query`, so the served `tools/list`
14
+ surface is **byte-identical in size** to 0.9.0; the frozen hashes were re-recorded and say why.
15
+ - Fix: the loose `http` argument object accepted and **silently ignored** any argument the tool does
16
+ not implement. Measured on 0.9.0: `asset_layout_id` (advertised by `mode:"resources"` as an assets
17
+ filter) returned unfiltered results with no signal — a dropped filter answers a different question
18
+ than the one asked. Unknown fields are now refused with a named error, and the looseness is kept
19
+ only for the policy-owned `tier` / `refresh`, where `meta.policyOverride` already discloses the
20
+ override. `stdio` stays strict.
21
+ - Documentation: the Typesense tier's 8-finding adversarial audit is closed. Seven were fixed in
22
+ `node-hudu` 0.22.0 (pre-scoring cap in Typesense's ranking order, non-numeric ids miscounted as a
23
+ page cap, a 401 from one collection masking a healthy sibling, the probe reading only `articles`,
24
+ the `hydrated` count's meaning, `verifyHits:false` disclosing nothing, and per-hit provenance now
25
+ carried by the required `source` field); Q8 was clean. Q7 (silent partial LRU body eviction under
26
+ `maxIndexTextBytes`) remains an unverified live-environment question and is not claimed as fixed.
27
+
28
+ ## 0.9.0
29
+
30
+ **Upgrading from 0.8.0:** no configuration change. The pinned `node-hudu` moves from 0.21.0 to 0.22.0,
31
+ which closes four MAJOR and three MINOR defects in the Typesense body tier found by an adversarial
32
+ audit. Nothing changes for a deployment that leaves the tier unset.
33
+
34
+ - Add: `probeBodySearch` results now carry the additive per-collection `results[]` field
35
+ (`node-hudu` 0.22.0). The probe report maps it when present, so a caller can see WHICH collection
36
+ answered or failed instead of only the aggregate `reachable`.
37
+ - Change: the pinned SDK's tier is honest about its bounds. Articles are hydrated in full through a
38
+ bounded pool of reads (so title matches ranked 11+ reach the local scorer), `meta.typesense` splits
39
+ `capped` / `unreadable` / `unscored`, the circuit breaker is per collection, a 401 from one
40
+ collection is no longer reported as a bad key when a sibling answered, and `verifyHits:false`
41
+ discloses that the tier contributed no hit. See `node-hudu` 0.22.0 release notes.
42
+
3
43
  ## 0.8.0
4
44
 
5
45
  **Upgrading from 0.7.0:** no configuration change for any existing deployment. Six new optional
@@ -384,7 +384,7 @@ function serverPolicyTierSentence(ctx) {
384
384
  /** The `query` field description, adjusted so it does not promise a body search it cannot run. */
385
385
  export function searchQueryDescription(ctx) {
386
386
  const { bodySearchable, toolDefaultTier } = ctx;
387
- const base = 'REQUIRED for mode="search". Partial words fine; article BODY is searched. Multi-word rules: topic="query" (mode="help").';
387
+ const base = 'REQUIRED for mode="search". Partial words fine; article BODY is searched. Multi-word: topic="query" (mode="help").';
388
388
  if (!bodySearchable) {
389
389
  // Fail loudly: a missed anchor would advertise a body search this deployment cannot run.
390
390
  if (!base.includes('article BODY is searched.'))
@@ -244,10 +244,12 @@ export function registerSearchTool(server, ctx) {
244
244
  // instead of being rejected before the policy path can answer. Under `stdio` every field is
245
245
  // served, so the object stays STRICT: an unknown argument is refused, never silently ignored.
246
246
  const inputFields = {
247
+ // The tool title already names the three modes; "help documents fields" is redundant there
248
+ // and costs 23 B of the measured surface. The help mode's own output documents fields.
247
249
  mode: z
248
250
  .enum(['search', 'help', 'resources'])
249
251
  .default('search')
250
- .describe('search|help|resources; help documents fields'),
252
+ .describe('search|help|resources'),
251
253
  query: z
252
254
  .string()
253
255
  .min(1)
@@ -265,12 +267,21 @@ export function registerSearchTool(server, ctx) {
265
267
  .describe('help only; "all" adds syntax'),
266
268
  limit: z.number().int().min(1).max(25).optional().describe('1-25, default 8; larger is CONFIG_ERROR'),
267
269
  snippetChars: z.number().int().min(0).max(400).optional().describe('0 = no snippet.'),
268
- company_id: z.number().int().positive().optional().describe('One company'),
269
- updated_since: z.string().optional().describe('ISO timestamp; newer only'),
270
+ // A name cannot be filtered: the tier sends company_id into Typesense's filter_by and the
271
+ // vendor path compares the number. Say so where the model reads it, and say what omission
272
+ // means, because "no company" is the wide answer, not a narrow one. Funded by trimming
273
+ // min_score/query prose so the served surface stays byte-identical (see the size gate).
274
+ company_id: z
275
+ .number()
276
+ .int()
277
+ .positive()
278
+ .optional()
279
+ .describe('Numeric company id; omit = all companies + global articles.'),
280
+ updated_since: z.string().optional().describe('ISO ts; newer only'),
270
281
  min_score: z
271
282
  .number()
272
283
  .optional()
273
- .describe('0-100; 0 hits may be floor/filter; meta.scanned/meta.returned; company_id/updated_since; drops uncounted.'),
284
+ .describe('0-100; floor may empty hits; meta.scanned/meta.returned; company_id/updated_since; uncounted.'),
274
285
  exact_only: z.boolean().optional().describe('No fuzzy match.'),
275
286
  // Under `http` neither field is served: the tier decides whether a call may wait for a
276
287
  // corpus-wide walk, and the calling model can observe neither the index's warmth nor the
@@ -321,6 +332,25 @@ export function registerSearchTool(server, ctx) {
321
332
  }
322
333
  // A field the mode cannot honour is a CONFIG_ERROR naming the fields it does honour.
323
334
  const bag = args;
335
+ // An argument this tool does not implement is REFUSED, never silently ignored. Under `http`
336
+ // the input object is loose ONLY so the policy-owned `tier`/`refresh` reach the handler and
337
+ // are reported as overridden (`meta.policyOverride`); every OTHER unknown key is a caller bug,
338
+ // and dropping it would answer a different question than the one asked. Measured on 0.9.0:
339
+ // `asset_layout_id` (advertised by `mode:"resources"` as an assets filter, but not implemented
340
+ // here) was accepted and ignored, returning unfiltered results with no signal. The invoke path
341
+ // already validates its bag against the SDK's inputSchema, so this aligns the two paths.
342
+ const knownFields = new Set([
343
+ ...Object.keys(inputFields),
344
+ ...(searchTierIsPolicy ? ['tier', 'refresh'] : []),
345
+ ]);
346
+ const unknownFields = Object.keys(bag).filter((k) => bag[k] !== undefined && !knownFields.has(k));
347
+ if (unknownFields.length) {
348
+ return errorContent(configError(
349
+ // Kept under the 200-char model-facing error cap (`MAX_ERROR_MESSAGE_CHARS`): a longer
350
+ // message is truncated mid-sentence, which is worse than a shorter one that survives
351
+ // intact. The essential disclosure comes first, the pointer last.
352
+ `hudu_search: unknown argument(s) ${unknownFields.join(', ')} — a dropped filter answers a different question, so unknown fields are never silently ignored. Call mode:"help" for fields.`));
353
+ }
324
354
  const provided = Object.keys(bag).filter((k) => k !== 'mode' && bag[k] !== undefined);
325
355
  const unhonoured = provided.filter((k) => spec.rejects.includes(k));
326
356
  if (unhonoured.length) {
@@ -51,6 +51,7 @@ export async function probeTypesense(hudu, config) {
51
51
  reachable: result.reachable === true,
52
52
  collections: Array.isArray(result.collections) && result.collections.length > 0 ? result.collections : collections,
53
53
  probeMs: typeof result.probeMs === 'number' ? result.probeMs : Date.now() - started,
54
+ ...(Array.isArray(result.results) && result.results.length > 0 ? { results: result.results } : {}),
54
55
  ...(result.error ? { error: { code: result.error.code, message: result.error.message } } : {}),
55
56
  };
56
57
  }
package/dist/tools.js CHANGED
@@ -32,4 +32,4 @@ export { MAX_SCOPED_CLIENTS, ScopedClientCache, createMcpServerFactory } from '.
32
32
  * it against the installed `CATALOG_PLAN_HASH`, so the upgrade fails CI instead of quietly shipping
33
33
  * a tool surface that no longer describes the SDK behind it.
34
34
  */
35
- export const BUILT_AGAINST_PLAN_HASH = '1c5f979de2bc4398d95e860dfa7adc43e00c2f5126233bc6b95d6aaa830b0cbb';
35
+ export const BUILT_AGAINST_PLAN_HASH = 'b46a003d8bef3f781f1bd1e21dc27271a1ad469eaa891560c767ad5b274dfaa1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iit-mcp-hudu",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "MCP server for Hudu IT documentation, built on the node-hudu SDK",
5
5
  "license": "MIT",
6
6
  "author": "Max Soukhomlinov",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@modelcontextprotocol/server": "^2.3.1",
46
- "node-hudu": "0.21.0",
46
+ "node-hudu": "0.22.0",
47
47
  "zod": "^4.6.7"
48
48
  },
49
49
  "devDependencies": {