@chrischall/mcp-utils 0.23.0 → 0.23.2
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 +10 -2
- package/dist/response/media.d.ts +17 -1
- package/dist/response/media.d.ts.map +1 -1
- package/dist/response/media.js +54 -9
- package/dist/response/media.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,7 +145,9 @@ from one, and a value that silently aliases to another is a lie in the schema.
|
|
|
145
145
|
`stripMediaUrls(payload)` is the highest-value projection that needs no
|
|
146
146
|
knowledge of the API: it drops `avatar` / `picture` / `cover_photo` /
|
|
147
147
|
`thumbnail` keys — including the `…Link` / `…Uri` / `…Url` suffixed forms every
|
|
148
|
-
Google Workspace API uses (`thumbnailLink`, `iconUri`, `photoUrl`)
|
|
148
|
+
Google Workspace API uses (`thumbnailLink`, `iconUri`, `photoUrl`), and the
|
|
149
|
+
snake_case and kebab-case forms most other APIs use (`image_url`,
|
|
150
|
+
`primary_photo_url`, `avatar_image_url`) — and bare
|
|
149
151
|
image URLs. The suffix is load-bearing outside consumer-social APIs: without it
|
|
150
152
|
the rule matched none of Google's media fields, and `thumbnailLink` alone is 32%
|
|
151
153
|
of a `gog drive ls` listing. Its key rule stays anchored at the START, so a key
|
|
@@ -161,9 +163,15 @@ mutates its input. Do **not** apply it to a tool whose product IS the image —
|
|
|
161
163
|
records are literally a `photoUrls` bundle) — where it empties the response
|
|
162
164
|
rather than shrinking it.
|
|
163
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
|
+
|
|
164
172
|
Two escape hatches, and they are symmetric: `keep` preserves a key that looks
|
|
165
173
|
like media but is the thing the caller asked for; `drop` adds keys this pattern
|
|
166
|
-
does not know
|
|
174
|
+
does not know. Both take `string | RegExp`, so a service with an unguessed naming convention can fix itself
|
|
167
175
|
**without a library release** — which is what Google Workspace's
|
|
168
176
|
`thumbnailLink`/`iconUri`/`photoUrl` cost the first time round. `keep` wins over
|
|
169
177
|
`drop`.
|
package/dist/response/media.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/response/media.js
CHANGED
|
@@ -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,
|
|
@@ -62,8 +66,29 @@
|
|
|
62
66
|
* node_modules. An earlier draft said 33 across the two and named `avatarUrls`
|
|
63
67
|
* as a third form; neither reproduced.)
|
|
64
68
|
*/
|
|
65
|
-
const MEDIA_NOUN = '(?:avatar|
|
|
66
|
-
|
|
69
|
+
const MEDIA_NOUN = '(?:avatar|picture|photo|thumbnail|thumb|image|icon|banner|profile_pic(?:ture)?|logo)';
|
|
70
|
+
/**
|
|
71
|
+
* A bounded set of qualifiers that may precede the noun in a snake_case or
|
|
72
|
+
* kebab-case key: `primary_photo_url`, `profile_image_url`, `hero-banner`.
|
|
73
|
+
*
|
|
74
|
+
* CLOSED on purpose, never `\w+`. That is the whole difference between this
|
|
75
|
+
* and the two clauses removed in #191 — a bare `\bavatar\b` and an
|
|
76
|
+
* `/avatars?/` path segment — which were open-ended, stripped genuine page
|
|
77
|
+
* URLs, and (measured) removed zero bytes. A media noun is also allowed here,
|
|
78
|
+
* for `avatar_image_url`.
|
|
79
|
+
*
|
|
80
|
+
* `cover` and `tall` live here rather than as `cover_photo` / `cover_image` /
|
|
81
|
+
* `tall_avatar` entries in MEDIA_NOUN. Those three were redundant once this
|
|
82
|
+
* list existed — `cover` + `photo` already composes — and keeping both spellings
|
|
83
|
+
* meant the snake_case form matched while the camelCase one silently did not.
|
|
84
|
+
*
|
|
85
|
+
* The separator is OPTIONAL for the same reason. With `[_-]` required,
|
|
86
|
+
* `cover_photo` was stripped and `coverPhoto` was kept: the same field, the
|
|
87
|
+
* same meaning, a different answer decided by an API's casing convention. That
|
|
88
|
+
* asymmetry is the exact shape of the bug #197 was about, one level up.
|
|
89
|
+
*/
|
|
90
|
+
const MEDIA_QUALIFIER = '(?:primary|secondary|main|default|cover|hero|profile|master|rendered|small|medium|large|full|original|tall)';
|
|
91
|
+
const MEDIA_KEY = new RegExp(`^(?:(?:${MEDIA_QUALIFIER}|${MEDIA_NOUN})[_-]?)?${MEDIA_NOUN}s?(?:[_-]?(?:link|uri|url|src)s?)?$`, 'i');
|
|
67
92
|
/**
|
|
68
93
|
* A URL that points at an image rather than at a page: a known image extension
|
|
69
94
|
* ending the PATH.
|
|
@@ -92,7 +117,9 @@ const MEDIA_URL = /^https?:\/\/[^\s]+?\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)([?
|
|
|
92
117
|
* The input is never mutated: several repos hand these helpers live cache rows.
|
|
93
118
|
*/
|
|
94
119
|
export function stripMediaUrls(value, opts = {}) {
|
|
95
|
-
|
|
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 ?? []);
|
|
96
123
|
// Every RegExp rule is COPIED, once per call, for two reasons.
|
|
97
124
|
//
|
|
98
125
|
// `test()` on a `g`- or `y`-flagged regex advances `lastIndex` on a match, so
|
|
@@ -107,13 +134,17 @@ export function stripMediaUrls(value, opts = {}) {
|
|
|
107
134
|
// caller's RegExp is input too. The docs invite hoisting `drop` as a shared
|
|
108
135
|
// constant, which is exactly when someone else's `lastIndex` would be ours to
|
|
109
136
|
// corrupt. Strings are lowercased here for the same once-per-call reason.
|
|
110
|
-
const drop = (opts.drop ?? [])
|
|
137
|
+
const drop = normalizeRules(opts.drop ?? []);
|
|
111
138
|
return walk(value, keep, drop);
|
|
112
139
|
}
|
|
113
|
-
/**
|
|
114
|
-
function
|
|
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) {
|
|
115
146
|
const lower = key.toLowerCase();
|
|
116
|
-
for (const rule of
|
|
147
|
+
for (const rule of rules) {
|
|
117
148
|
if (typeof rule === 'string') {
|
|
118
149
|
if (rule === lower)
|
|
119
150
|
return true;
|
|
@@ -127,6 +158,20 @@ function alsoDrop(key, drop) {
|
|
|
127
158
|
return false;
|
|
128
159
|
}
|
|
129
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.
|
|
130
175
|
if (Array.isArray(value))
|
|
131
176
|
return value.map((v) => walk(v, keep, drop));
|
|
132
177
|
// `null` is data here, not an empty object — see the docblock.
|
|
@@ -139,11 +184,11 @@ function walk(value, keep, drop) {
|
|
|
139
184
|
return value;
|
|
140
185
|
const out = {};
|
|
141
186
|
for (const [key, v] of Object.entries(value)) {
|
|
142
|
-
if (
|
|
187
|
+
if (matchesRule(key, keep)) {
|
|
143
188
|
out[key] = v;
|
|
144
189
|
continue;
|
|
145
190
|
}
|
|
146
|
-
if (MEDIA_KEY.test(key) ||
|
|
191
|
+
if (MEDIA_KEY.test(key) || matchesRule(key, drop))
|
|
147
192
|
continue;
|
|
148
193
|
if (typeof v === 'string' && MEDIA_URL.test(v))
|
|
149
194
|
continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/response/media.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/response/media.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/mcp-utils",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.2",
|
|
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",
|