@chrischall/mcp-utils 0.22.0 → 0.23.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/README.md +18 -3
- package/dist/response/media.d.ts +16 -0
- package/dist/response/media.d.ts.map +1 -1
- package/dist/response/media.js +68 -7
- package/dist/response/media.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,14 +144,29 @@ from one, and a value that silently aliases to another is a lie in the schema.
|
|
|
144
144
|
|
|
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
|
-
`thumbnail` keys
|
|
147
|
+
`thumbnail` keys — including the `…Link` / `…Uri` / `…Url` suffixed forms every
|
|
148
|
+
Google Workspace API uses (`thumbnailLink`, `iconUri`, `photoUrl`) — and bare
|
|
149
|
+
image URLs. The suffix is load-bearing outside consumer-social APIs: without it
|
|
150
|
+
the rule matched none of Google's media fields, and `thumbnailLink` alone is 32%
|
|
151
|
+
of a `gog drive ls` listing. Its key rule stays anchored at the START, so a key
|
|
152
|
+
that merely contains a media noun survives — Drive's `hasThumbnail: false` is a
|
|
153
|
+
fact about the file, and `webViewLink` sits in the same object as
|
|
154
|
+
`thumbnailLink`. Measured on a real 187.6 KB
|
|
148
155
|
`splitwise-mcp` groups response — which does not fit in a tool result at all —
|
|
149
156
|
minifying alone is −25%, minifying **and** stripping media is **−73%**. It
|
|
150
157
|
deliberately keeps `null` (an absent key and a null one are different facts;
|
|
151
158
|
`ofw-mcp`'s `viewedAt: null` means "never opened"), keeps page URLs, and never
|
|
152
159
|
mutates its input. Do **not** apply it to a tool whose product IS the image —
|
|
153
|
-
`alltrails_get_trail_photos`, `sw_get_receipt`
|
|
154
|
-
|
|
160
|
+
`alltrails_get_trail_photos`, `sw_get_receipt`, `redfin`'s photo tools (whose
|
|
161
|
+
records are literally a `photoUrls` bundle) — where it empties the response
|
|
162
|
+
rather than shrinking it.
|
|
163
|
+
|
|
164
|
+
Two escape hatches, and they are symmetric: `keep` preserves a key that looks
|
|
165
|
+
like media but is the thing the caller asked for; `drop` adds keys this pattern
|
|
166
|
+
does not know, so a service with an unguessed naming convention can fix itself
|
|
167
|
+
**without a library release** — which is what Google Workspace's
|
|
168
|
+
`thumbnailLink`/`iconUri`/`photoUrl` cost the first time round. `keep` wins over
|
|
169
|
+
`drop`.
|
|
155
170
|
|
|
156
171
|
`viewResult` minifies `compact` and `full` and leaves `raw` indented (that rung
|
|
157
172
|
exists to be read by a person); `minifiedResult` is the same rule with no view
|
package/dist/response/media.d.ts
CHANGED
|
@@ -38,6 +38,22 @@ export interface StripMediaOptions {
|
|
|
38
38
|
* decorative avatar with an image the caller actually asked for.
|
|
39
39
|
*/
|
|
40
40
|
keep?: readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* Extra keys to drop, for a service whose naming this pattern does not know.
|
|
43
|
+
*
|
|
44
|
+
* The symmetric half of `keep`, and the point is that a repo should not need
|
|
45
|
+
* a LIBRARY RELEASE to strip its own noise. That is exactly what happened
|
|
46
|
+
* once already: Google Workspace names every picture `thumbnailLink` /
|
|
47
|
+
* `iconUri` / `photoUrl`, none of which the original pattern matched, and
|
|
48
|
+
* closing it took a version bump across every consumer. The next service with
|
|
49
|
+
* an unguessed convention can now fix itself locally and propose the pattern
|
|
50
|
+
* change at leisure.
|
|
51
|
+
*
|
|
52
|
+
* A string matches a key exactly, case-insensitively; a RegExp is tested
|
|
53
|
+
* against the key as given, whatever flags it carries — see `stripMediaUrls`
|
|
54
|
+
* for why a `g` or `y` rule needs care.
|
|
55
|
+
*/
|
|
56
|
+
drop?: readonly (string | RegExp)[];
|
|
41
57
|
}
|
|
42
58
|
/**
|
|
43
59
|
* Return a copy of `value` with media keys and bare image URLs removed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/response/media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/response/media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAuDH,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"}
|
package/dist/response/media.js
CHANGED
|
@@ -32,8 +32,38 @@
|
|
|
32
32
|
* tool's own name is the test. `keep` is the escape hatch for a payload that
|
|
33
33
|
* mixes both.
|
|
34
34
|
*/
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Keys whose value is a picture, whatever it holds.
|
|
37
|
+
*
|
|
38
|
+
* The optional `Link|Uri|Url` suffix is what makes this work outside
|
|
39
|
+
* consumer-social APIs. Splitwise names a picture `avatar`; every Google
|
|
40
|
+
* Workspace API names it `thumbnailLink`, `iconUri`, `photoUrl`. Without the
|
|
41
|
+
* suffix this rule matched NONE of them — measured on a real `gog drive ls`
|
|
42
|
+
* listing of 25 rows, `thumbnailLink` alone was 4,973 bytes of 15,698 (32% of
|
|
43
|
+
* the payload) and this helper removed zero of it. Nor does MEDIA_URL cover the
|
|
44
|
+
* gap: Google's media URLs are extension-less signed URLs. Adding the suffix
|
|
45
|
+
* takes that listing down 31%, and `drive search` — which supports no field
|
|
46
|
+
* mask at all, so nothing else can shrink it — down 30%.
|
|
47
|
+
*
|
|
48
|
+
* The anchor stays at the START, which is the whole safety property. A key that
|
|
49
|
+
* merely CONTAINS a media noun is untouched, so Drive's `hasThumbnail: false`
|
|
50
|
+
* survives: it is a fact about the file, and a caller filtering on it would
|
|
51
|
+
* otherwise see the key vanish and read that as "not reported". Only the three
|
|
52
|
+
* reference suffixes are added, so `thumbnailWidth` (a number) and
|
|
53
|
+
* `imageMediaMetadata` (EXIF) stay too — and so does `webViewLink`, whose noun
|
|
54
|
+
* is not media and which sits in the same object as `thumbnailLink`.
|
|
55
|
+
*
|
|
56
|
+
* The trailing `s?` after the suffix closes a gap the suffix opened: the
|
|
57
|
+
* pattern matched `imageUrl` and `images`, but not `imageUrls`. Those plurals
|
|
58
|
+
* are not hypothetical — `imageUrls` appears 5 times in groupon-mcp and
|
|
59
|
+
* `photoUrls` 17 times in redfin-mcp — and a rule that catches the singular
|
|
60
|
+
* while missing the plural of the same field is the kind of half-cover that
|
|
61
|
+
* reads as working. (Counted with `grep -rhoE` over both repos excluding
|
|
62
|
+
* node_modules. An earlier draft said 33 across the two and named `avatarUrls`
|
|
63
|
+
* as a third form; neither reproduced.)
|
|
64
|
+
*/
|
|
65
|
+
const MEDIA_NOUN = '(?:avatar|tall_avatar|cover_photo|cover_image|picture|photo|thumbnail|thumb|image|icon|banner|profile_pic(?:ture)?|logo)';
|
|
66
|
+
const MEDIA_KEY = new RegExp(`^${MEDIA_NOUN}s?(?:(?:link|uri|url)s?)?$`, 'i');
|
|
37
67
|
/**
|
|
38
68
|
* A URL that points at an image rather than at a page: a known image extension
|
|
39
69
|
* ending the PATH.
|
|
@@ -63,11 +93,42 @@ const MEDIA_URL = /^https?:\/\/[^\s]+?\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)([?
|
|
|
63
93
|
*/
|
|
64
94
|
export function stripMediaUrls(value, opts = {}) {
|
|
65
95
|
const keep = new Set((opts.keep ?? []).map((k) => k.toLowerCase()));
|
|
66
|
-
|
|
96
|
+
// Every RegExp rule is COPIED, once per call, for two reasons.
|
|
97
|
+
//
|
|
98
|
+
// `test()` on a `g`- or `y`-flagged regex advances `lastIndex` on a match, so
|
|
99
|
+
// one rule object tested across a sequence of keys silently skips the next key
|
|
100
|
+
// whose length falls inside the advanced index. It fails OPEN — the key
|
|
101
|
+
// survives — and which key survives depends on the ORDER they are walked in,
|
|
102
|
+
// so it reads as "the rule did not match" rather than as a bug. Nothing in the
|
|
103
|
+
// type forbids a `/g`, and `/^blur/gi` is a natural thing to write.
|
|
104
|
+
//
|
|
105
|
+
// Copying rather than resetting the caller's own regex keeps the promise this
|
|
106
|
+
// helper already makes about payloads: it never mutates its input, and a
|
|
107
|
+
// caller's RegExp is input too. The docs invite hoisting `drop` as a shared
|
|
108
|
+
// constant, which is exactly when someone else's `lastIndex` would be ours to
|
|
109
|
+
// corrupt. Strings are lowercased here for the same once-per-call reason.
|
|
110
|
+
const drop = (opts.drop ?? []).map((rule) => typeof rule === 'string' ? rule.toLowerCase() : new RegExp(rule.source, rule.flags));
|
|
111
|
+
return walk(value, keep, drop);
|
|
112
|
+
}
|
|
113
|
+
/** Does `key` match one of the caller's extra drop rules? */
|
|
114
|
+
function alsoDrop(key, drop) {
|
|
115
|
+
const lower = key.toLowerCase();
|
|
116
|
+
for (const rule of drop) {
|
|
117
|
+
if (typeof rule === 'string') {
|
|
118
|
+
if (rule === lower)
|
|
119
|
+
return true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Our own copy, so resetting is free and invisible to the caller.
|
|
123
|
+
rule.lastIndex = 0;
|
|
124
|
+
if (rule.test(key))
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
67
128
|
}
|
|
68
|
-
function walk(value, keep) {
|
|
129
|
+
function walk(value, keep, drop) {
|
|
69
130
|
if (Array.isArray(value))
|
|
70
|
-
return value.map((v) => walk(v, keep));
|
|
131
|
+
return value.map((v) => walk(v, keep, drop));
|
|
71
132
|
// `null` is data here, not an empty object — see the docblock.
|
|
72
133
|
if (value === null || typeof value !== 'object')
|
|
73
134
|
return value;
|
|
@@ -82,11 +143,11 @@ function walk(value, keep) {
|
|
|
82
143
|
out[key] = v;
|
|
83
144
|
continue;
|
|
84
145
|
}
|
|
85
|
-
if (MEDIA_KEY.test(key))
|
|
146
|
+
if (MEDIA_KEY.test(key) || alsoDrop(key, drop))
|
|
86
147
|
continue;
|
|
87
148
|
if (typeof v === 'string' && MEDIA_URL.test(v))
|
|
88
149
|
continue;
|
|
89
|
-
out[key] = walk(v, keep);
|
|
150
|
+
out[key] = walk(v, keep, drop);
|
|
90
151
|
}
|
|
91
152
|
return out;
|
|
92
153
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/response/media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,
|
|
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.
|
|
3
|
+
"version": "0.23.0",
|
|
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",
|