@chrischall/mcp-utils 0.23.1 → 0.23.3

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/README.md CHANGED
@@ -163,9 +163,15 @@ mutates its input. Do **not** apply it to a tool whose product IS the image —
163
163
  records are literally a `photoUrls` bundle) — where it empties the response
164
164
  rather than shrinking it.
165
165
 
166
+ **Arrays of bare image URLs under a non-media key are kept** — `floorplan_urls:
167
+ ['a.jpg', 'b.jpg']` comes back whole. That is deliberate: removing a *key* is
168
+ visible, removing *elements* is not, and a caller reading `.length` to report
169
+ "4 floor plans" would be quietly wrong. Use `drop` for those. An array under a
170
+ media-named key (`photos: [...]`) is already removed by the key rule.
171
+
166
172
  Two escape hatches, and they are symmetric: `keep` preserves a key that looks
167
173
  like media but is the thing the caller asked for; `drop` adds keys this pattern
168
- does not know, so a service with an unguessed naming convention can fix itself
174
+ does not know. Both take `string | RegExp`, so a service with an unguessed naming convention can fix itself
169
175
  **without a library release** — which is what Google Workspace's
170
176
  `thumbnailLink`/`iconUri`/`photoUrl` cost the first time round. `keep` wins over
171
177
  `drop`.
@@ -25,6 +25,10 @@
25
25
  * answered. Three points is not worth a payload that can no longer distinguish
26
26
  * "no value" from "not reported".
27
27
  *
28
+ * **Arrays of bare image URLs under a non-media key are KEPT** — see the note
29
+ * in `walk`. Use `drop` for those; the key rule already covers an array under
30
+ * a media-named key (`photos: [...]`).
31
+ *
28
32
  * **Never apply this to a tool whose PRODUCT is the image.**
29
33
  * `alltrails_get_trail_photos`, `zillow_get_property_photos`,
30
34
  * `sw_get_receipt`, `musescore_fetch_svgs` exist to return exactly these URLs,
@@ -36,8 +40,20 @@ export interface StripMediaOptions {
36
40
  /**
37
41
  * Keys to keep even when they look like media — for a payload that mixes a
38
42
  * decorative avatar with an image the caller actually asked for.
43
+ *
44
+ * Takes the same `string | RegExp` shapes as `drop`. It did not until this
45
+ * was noticed: `drop` gained RegExp support in #194 and `keep` was left on
46
+ * strings, so a repo preserving several CONSTRUCTED media fields that share a
47
+ * prefix — redfin-mcp's `image_url` + `thumbnail_url`, compass-mcp's
48
+ * `primary_photo_url` + `primary_thumbnail_url` — had to enumerate each one
49
+ * while the opposite intent could be a pattern.
50
+ *
51
+ * That was an oversight in the API rather than a judgement, and if either
52
+ * side deserved the expressiveness first it was this one: a missed `drop`
53
+ * entry leaves bytes in the response, while a missed `keep` entry silently
54
+ * deletes a field the caller asked for.
39
55
  */
40
- keep?: readonly string[];
56
+ keep?: readonly (string | RegExp)[];
41
57
  /**
42
58
  * Extra keys to drop, for a service whose naming this pattern does not know.
43
59
  *
@@ -1 +1 @@
1
- {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/response/media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAiFH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB;;;;;;;;;;;;;;OAcG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CACrC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,GAAE,iBAAsB,GAAG,CAAC,CAmB3E"}
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/response/media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAiFH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;;;;;;OAeG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACpC;;;;;;;;;;;;;;OAcG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CACrC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,GAAE,iBAAsB,GAAG,CAAC,CAoB3E"}
@@ -25,6 +25,10 @@
25
25
  * answered. Three points is not worth a payload that can no longer distinguish
26
26
  * "no value" from "not reported".
27
27
  *
28
+ * **Arrays of bare image URLs under a non-media key are KEPT** — see the note
29
+ * in `walk`. Use `drop` for those; the key rule already covers an array under
30
+ * a media-named key (`photos: [...]`).
31
+ *
28
32
  * **Never apply this to a tool whose PRODUCT is the image.**
29
33
  * `alltrails_get_trail_photos`, `zillow_get_property_photos`,
30
34
  * `sw_get_receipt`, `musescore_fetch_svgs` exist to return exactly these URLs,
@@ -113,7 +117,9 @@ const MEDIA_URL = /^https?:\/\/[^\s]+?\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)([?
113
117
  * The input is never mutated: several repos hand these helpers live cache rows.
114
118
  */
115
119
  export function stripMediaUrls(value, opts = {}) {
116
- const keep = new Set((opts.keep ?? []).map((k) => k.toLowerCase()));
120
+ // `keep` is normalised exactly like `drop` below — same copy-the-regex
121
+ // reasoning, same once-per-call lowercasing.
122
+ const keep = normalizeRules(opts.keep ?? []);
117
123
  // Every RegExp rule is COPIED, once per call, for two reasons.
118
124
  //
119
125
  // `test()` on a `g`- or `y`-flagged regex advances `lastIndex` on a match, so
@@ -128,13 +134,17 @@ export function stripMediaUrls(value, opts = {}) {
128
134
  // caller's RegExp is input too. The docs invite hoisting `drop` as a shared
129
135
  // constant, which is exactly when someone else's `lastIndex` would be ours to
130
136
  // corrupt. Strings are lowercased here for the same once-per-call reason.
131
- const drop = (opts.drop ?? []).map((rule) => typeof rule === 'string' ? rule.toLowerCase() : new RegExp(rule.source, rule.flags));
137
+ const drop = normalizeRules(opts.drop ?? []);
132
138
  return walk(value, keep, drop);
133
139
  }
134
- /** Does `key` match one of the caller's extra drop rules? */
135
- function alsoDrop(key, drop) {
140
+ /** Lowercase the strings and copy the regexes, once per call. See above. */
141
+ function normalizeRules(rules) {
142
+ return rules.map((rule) => (typeof rule === 'string' ? rule.toLowerCase() : new RegExp(rule.source, rule.flags)));
143
+ }
144
+ /** Does `key` match one of the caller's rules? Used for both `keep` and `drop`. */
145
+ function matchesRule(key, rules) {
136
146
  const lower = key.toLowerCase();
137
- for (const rule of drop) {
147
+ for (const rule of rules) {
138
148
  if (typeof rule === 'string') {
139
149
  if (rule === lower)
140
150
  return true;
@@ -148,6 +158,20 @@ function alsoDrop(key, drop) {
148
158
  return false;
149
159
  }
150
160
  function walk(value, keep, drop) {
161
+ // Array ELEMENTS are walked but never value-tested, so an array of bare image
162
+ // URLs under a non-media key — homes-mcp's `floorplan_urls` — comes back
163
+ // whole. That is deliberate, and it is the one place this helper knowingly
164
+ // leaves bytes on the table.
165
+ //
166
+ // Removing a KEY is visible: the field is gone and a reader can see that it
167
+ // is. Removing ELEMENTS is invisible — the array is merely shorter, and a
168
+ // caller reading `floorplan_urls.length` to say "this listing has 4 floor
169
+ // plans" would be quietly wrong. That is the same class of harm as the
170
+ // dropped-nulls rule this helper also refuses (see the docblock above):
171
+ // a silently altered count reads as fact.
172
+ //
173
+ // The fix for such a field is `drop: ['floorplan_urls']` at the call site,
174
+ // which removes the key outright and stays legible in the response.
151
175
  if (Array.isArray(value))
152
176
  return value.map((v) => walk(v, keep, drop));
153
177
  // `null` is data here, not an empty object — see the docblock.
@@ -160,11 +184,11 @@ function walk(value, keep, drop) {
160
184
  return value;
161
185
  const out = {};
162
186
  for (const [key, v] of Object.entries(value)) {
163
- if (keep.has(key.toLowerCase())) {
187
+ if (matchesRule(key, keep)) {
164
188
  out[key] = v;
165
189
  continue;
166
190
  }
167
- if (MEDIA_KEY.test(key) || alsoDrop(key, drop))
191
+ if (MEDIA_KEY.test(key) || matchesRule(key, drop))
168
192
  continue;
169
193
  if (typeof v === 'string' && MEDIA_URL.test(v))
170
194
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/response/media.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/response/media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,GAAG,sFAAsF,CAAC;AAE1G;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,eAAe,GACnB,6GAA6G,CAAC;AAEhH,MAAM,SAAS,GAAG,IAAI,MAAM,CAC1B,UAAU,eAAe,IAAI,UAAU,WAAW,UAAU,qCAAqC,EACjG,GAAG,CACJ,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,SAAS,GAAG,qEAAqE,CAAC;AAsCxF;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAI,KAAQ,EAAE,OAA0B,EAAE;IACtE,uEAAuE;IACvE,6CAA6C;IAC7C,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC7C,+DAA+D;IAC/D,EAAE;IACF,8EAA8E;IAC9E,+EAA+E;IAC/E,wEAAwE;IACxE,6EAA6E;IAC7E,+EAA+E;IAC/E,oEAAoE;IACpE,EAAE;IACF,8EAA8E;IAC9E,yEAAyE;IACzE,4EAA4E;IAC5E,8EAA8E;IAC9E,0EAA0E;IAC1E,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC7C,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAM,CAAC;AACtC,CAAC;AAED,4EAA4E;AAC5E,SAAS,cAAc,CAAC,KAAmC;IACzD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpH,CAAC;AAED,mFAAmF;AACnF,SAAS,WAAW,CAAC,GAAW,EAAE,KAAmC;IACnE,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,IAAI,IAAI,KAAK,KAAK;gBAAE,OAAO,IAAI,CAAC;YAChC,SAAS;QACX,CAAC;QACD,kEAAkE;QAClE,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;QACnB,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;IAClC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,IAAI,CAAC,KAAc,EAAE,IAAkC,EAAE,IAAkC;IAClG,8EAA8E;IAC9E,yEAAyE;IACzE,2EAA2E;IAC3E,6BAA6B;IAC7B,EAAE;IACF,4EAA4E;IAC5E,0EAA0E;IAC1E,0EAA0E;IAC1E,uEAAuE;IACvE,wEAAwE;IACxE,0CAA0C;IAC1C,EAAE;IACF,2EAA2E;IAC3E,oEAAoE;IACpE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IACvE,+DAA+D;IAC/D,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9D,6EAA6E;IAC7E,6EAA6E;IAC7E,sCAAsC;IACtC,IAAI,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC7G,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;QACxE,IAAI,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC;YAC3B,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACb,SAAS;QACX,CAAC;QACD,IAAI,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC;YAAE,SAAS;QAC5D,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,SAAS;QACzD,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/mcp-utils",
3
- "version": "0.23.1",
3
+ "version": "0.23.3",
4
4
  "description": "Shared scaffolding for the chrischall MCP fleet — server bootstrap, tool-result formatting, helpful errors, hardened env/config, a bearer API-client kit, zod atoms, session registries, a fetchproxy transport adapter, auth resolver skeletons, an in-memory test harness, and opt-in HTML helpers. The generic MCP glue hoisted out of ~19 sibling servers.",
5
5
  "type": "module",
6
6
  "license": "MIT",