@chrischall/mcp-utils 0.21.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 CHANGED
@@ -108,7 +108,7 @@ deepMapStringField(payload, 'eventDate', dmyToIso);
108
108
  #### The `view` vocabulary — read tools answer in the cheap shape by default
109
109
 
110
110
  `VIEWS`, `DEFAULT_VIEW`, `View`, `viewParam`, `resolveView`, `viewResult`,
111
- `minifiedResult`, `projectOrRaw`. See `docs/fleet-conventions.md`
111
+ `minifiedResult`, `projectOrRaw`, `stripMediaUrls`. See `docs/fleet-conventions.md`
112
112
  ("Response shape") for the convention these implement.
113
113
 
114
114
  `view: 'compact' | 'full' | 'raw'`, defaulting to **`compact`** — a projection
@@ -142,6 +142,32 @@ where a record is *assembled* from several endpoints rather than passed through
142
142
  from one, and a value that silently aliases to another is a lie in the schema.
143
143
  `raw` means "no projection" — never "no normalisation".
144
144
 
145
+ `stripMediaUrls(payload)` is the highest-value projection that needs no
146
+ knowledge of the API: it drops `avatar` / `picture` / `cover_photo` /
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
155
+ `splitwise-mcp` groups response — which does not fit in a tool result at all —
156
+ minifying alone is −25%, minifying **and** stripping media is **−73%**. It
157
+ deliberately keeps `null` (an absent key and a null one are different facts;
158
+ `ofw-mcp`'s `viewedAt: null` means "never opened"), keeps page URLs, and never
159
+ mutates its input. Do **not** apply it to a tool whose product IS the image —
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`.
170
+
145
171
  `viewResult` minifies `compact` and `full` and leaves `raw` indented (that rung
146
172
  exists to be read by a person); `minifiedResult` is the same rule with no view
147
173
  to hand. Formatting whitespace only — whitespace *inside* a value is content
@@ -49,4 +49,5 @@ export declare function pruneUndefined<T extends Record<string, unknown>>(obj: T
49
49
  */
50
50
  export declare function toArray<T>(value: T | T[] | null | undefined): T[];
51
51
  export * from './view.js';
52
+ export * from './media.js';
52
53
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/response/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAIzE;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAIxD;AAED,sEAAsE;AACtE,eAAO,MAAM,UAAU,mBAAa,CAAC;AAErC,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAE1D;AAED,yDAAyD;AACzD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,CAI5E;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAK3D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAoBxD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,CAAC,CAY5F;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMpF;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,CAAC,EAAE,CAGjE;AACD,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/response/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAIzE;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAIxD;AAED,sEAAsE;AACtE,eAAO,MAAM,UAAU,mBAAa,CAAC;AAErC,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAE1D;AAED,yDAAyD;AACzD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,CAI5E;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAK3D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAoBxD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,CAAC,CAY5F;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMpF;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,CAAC,EAAE,CAGjE;AACD,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
@@ -116,4 +116,5 @@ export function toArray(value) {
116
116
  return Array.isArray(value) ? value : [value];
117
117
  }
118
118
  export * from './view.js';
119
+ export * from './media.js';
119
120
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/response/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/response/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Drop image and avatar references from a payload.
3
+ *
4
+ * This is the single highest-value, lowest-risk projection in the fleet, and it
5
+ * needs no knowledge of the API at all. Measured on a real
6
+ * `splitwise-mcp sw_list_groups` response — 51 groups, 187.6 KB, which does not
7
+ * fit in a tool result at all and fails outright:
8
+ *
9
+ * | | size | |
10
+ * |---|---|---|
11
+ * | as shipped (pretty) | 187.6 KB | — |
12
+ * | minified | 140.5 KB | −25% |
13
+ * | minified + media stripped | **51.4 KB** | **−73%** |
14
+ *
15
+ * 60% of that payload was `avatar`, `tall_avatar`, `cover_photo` and a
16
+ * `picture` per member — URLs a model cannot see, cannot fetch, and would not
17
+ * benefit from if it could.
18
+ *
19
+ * **Why not also drop nulls.** It was the obvious companion rule and it is
20
+ * rejected on purpose: measured on the same payload it buys three further
21
+ * points (−73% → −75%), and it costs meaning. `ofw-mcp` emits
22
+ * `recipients[].viewedAt: null` to say "this person has never opened it",
23
+ * which is evidence in a custody record; an absent key and a null one are the
24
+ * same to `JSON.parse` but not to a reader deciding whether the question was
25
+ * answered. Three points is not worth a payload that can no longer distinguish
26
+ * "no value" from "not reported".
27
+ *
28
+ * **Never apply this to a tool whose PRODUCT is the image.**
29
+ * `alltrails_get_trail_photos`, `zillow_get_property_photos`,
30
+ * `sw_get_receipt`, `musescore_fetch_svgs` exist to return exactly these URLs,
31
+ * and stripping them there does not shrink the response, it empties it. The
32
+ * tool's own name is the test. `keep` is the escape hatch for a payload that
33
+ * mixes both.
34
+ */
35
+ export interface StripMediaOptions {
36
+ /**
37
+ * Keys to keep even when they look like media — for a payload that mixes a
38
+ * decorative avatar with an image the caller actually asked for.
39
+ */
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)[];
57
+ }
58
+ /**
59
+ * Return a copy of `value` with media keys and bare image URLs removed.
60
+ *
61
+ * Recurses through arrays and plain objects. Everything else — strings,
62
+ * numbers, booleans, **null**, Dates — is passed through untouched, so a
63
+ * `null` that means "never viewed" survives.
64
+ *
65
+ * The input is never mutated: several repos hand these helpers live cache rows.
66
+ */
67
+ export declare function stripMediaUrls<T>(value: T, opts?: StripMediaOptions): T;
68
+ //# sourceMappingURL=media.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Drop image and avatar references from a payload.
3
+ *
4
+ * This is the single highest-value, lowest-risk projection in the fleet, and it
5
+ * needs no knowledge of the API at all. Measured on a real
6
+ * `splitwise-mcp sw_list_groups` response — 51 groups, 187.6 KB, which does not
7
+ * fit in a tool result at all and fails outright:
8
+ *
9
+ * | | size | |
10
+ * |---|---|---|
11
+ * | as shipped (pretty) | 187.6 KB | — |
12
+ * | minified | 140.5 KB | −25% |
13
+ * | minified + media stripped | **51.4 KB** | **−73%** |
14
+ *
15
+ * 60% of that payload was `avatar`, `tall_avatar`, `cover_photo` and a
16
+ * `picture` per member — URLs a model cannot see, cannot fetch, and would not
17
+ * benefit from if it could.
18
+ *
19
+ * **Why not also drop nulls.** It was the obvious companion rule and it is
20
+ * rejected on purpose: measured on the same payload it buys three further
21
+ * points (−73% → −75%), and it costs meaning. `ofw-mcp` emits
22
+ * `recipients[].viewedAt: null` to say "this person has never opened it",
23
+ * which is evidence in a custody record; an absent key and a null one are the
24
+ * same to `JSON.parse` but not to a reader deciding whether the question was
25
+ * answered. Three points is not worth a payload that can no longer distinguish
26
+ * "no value" from "not reported".
27
+ *
28
+ * **Never apply this to a tool whose PRODUCT is the image.**
29
+ * `alltrails_get_trail_photos`, `zillow_get_property_photos`,
30
+ * `sw_get_receipt`, `musescore_fetch_svgs` exist to return exactly these URLs,
31
+ * and stripping them there does not shrink the response, it empties it. The
32
+ * tool's own name is the test. `keep` is the escape hatch for a payload that
33
+ * mixes both.
34
+ */
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');
67
+ /**
68
+ * A URL that points at an image rather than at a page: a known image extension
69
+ * ending the PATH.
70
+ *
71
+ * Nothing looser. Earlier drafts also matched an `avatar` path segment and a
72
+ * bare `avatar` word, and both were false positives waiting to happen —
73
+ * `…/users/avatar-collection` and `…/v1/avatar/settings` are pages, and this
74
+ * helper's whole promise is that it keeps pages. Measured on the payload that
75
+ * motivated the helper, the extra clauses removed exactly ZERO additional
76
+ * bytes: every avatar in a real Splitwise response is either under a media KEY
77
+ * (caught by `MEDIA_KEY`, whatever the URL looks like) or ends in `.png`/`.jpg`.
78
+ * A rule that adds risk and removes nothing is not a rule.
79
+ *
80
+ * The extension must END the path, so a signed URL whose query happens to
81
+ * contain `.jpeg` — Splitwise's `…/receipt?cachebust=29f.jpeg&size=large` — is
82
+ * KEPT. That one is content a caller asked for, not decoration.
83
+ */
84
+ const MEDIA_URL = /^https?:\/\/[^\s]+?\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)([?#]|$)/i;
85
+ /**
86
+ * Return a copy of `value` with media keys and bare image URLs removed.
87
+ *
88
+ * Recurses through arrays and plain objects. Everything else — strings,
89
+ * numbers, booleans, **null**, Dates — is passed through untouched, so a
90
+ * `null` that means "never viewed" survives.
91
+ *
92
+ * The input is never mutated: several repos hand these helpers live cache rows.
93
+ */
94
+ export function stripMediaUrls(value, opts = {}) {
95
+ const keep = new Set((opts.keep ?? []).map((k) => k.toLowerCase()));
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;
128
+ }
129
+ function walk(value, keep, drop) {
130
+ if (Array.isArray(value))
131
+ return value.map((v) => walk(v, keep, drop));
132
+ // `null` is data here, not an empty object — see the docblock.
133
+ if (value === null || typeof value !== 'object')
134
+ return value;
135
+ // Anything with a prototype of its own (Date, Map, a class instance) is left
136
+ // alone: rebuilding it from its enumerable keys would quietly change what it
137
+ // is, and none of them carry avatars.
138
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
139
+ return value;
140
+ const out = {};
141
+ for (const [key, v] of Object.entries(value)) {
142
+ if (keep.has(key.toLowerCase())) {
143
+ out[key] = v;
144
+ continue;
145
+ }
146
+ if (MEDIA_KEY.test(key) || alsoDrop(key, drop))
147
+ continue;
148
+ if (typeof v === 'string' && MEDIA_URL.test(v))
149
+ continue;
150
+ out[key] = walk(v, keep, drop);
151
+ }
152
+ return out;
153
+ }
154
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
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.21.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",