@chrischall/mcp-utils 0.20.0 → 0.21.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
@@ -105,6 +105,48 @@ return textResult(flattenJsonApi(payload)); // collapse JSON:API envelopes
105
105
  deepMapStringField(payload, 'eventDate', dmyToIso);
106
106
  ```
107
107
 
108
+ #### The `view` vocabulary — read tools answer in the cheap shape by default
109
+
110
+ `VIEWS`, `DEFAULT_VIEW`, `View`, `viewParam`, `resolveView`, `viewResult`,
111
+ `minifiedResult`, `projectOrRaw`. See `docs/fleet-conventions.md`
112
+ ("Response shape") for the convention these implement.
113
+
114
+ `view: 'compact' | 'full' | 'raw'`, defaulting to **`compact`** — a projection
115
+ that has to be requested is one that usually is not.
116
+
117
+ ```ts
118
+ import { viewParam, resolveView, viewResult, projectOrRaw } from '@chrischall/mcp-utils';
119
+
120
+ const VIEWS_HERE = ['compact', 'full'] as const; // only the rungs you honour
121
+
122
+ server.registerTool('svc_list_things', {
123
+ inputSchema: {
124
+ view: viewParam(VIEWS_HERE, { note: 'compact omits the upstream `meta` echo.' }),
125
+ },
126
+ }, async (args) => {
127
+ const view = resolveView(args.view, VIEWS_HERE);
128
+ const rows = await client.list();
129
+ // Project the ARRAY, so one odd record cannot half-answer, and fall back to
130
+ // the whole payload (warning to stderr) if the upstream shape has drifted.
131
+ const items = view === 'compact'
132
+ ? projectOrRaw(rows, (rs) => rs.map(compactThing), { label: 'svc-mcp', context: 'GET /things' })
133
+ : rows;
134
+ return viewResult(view, { count: rows.length, items });
135
+ });
136
+ ```
137
+
138
+ `viewParam` refuses a rung list without `compact` (a tool with no cheap answer
139
+ has nothing to default to) and refuses a single-rung list (a parameter that
140
+ decides nothing). Register only the rungs you honour: `raw` is meaningless
141
+ where a record is *assembled* from several endpoints rather than passed through
142
+ from one, and a value that silently aliases to another is a lie in the schema.
143
+ `raw` means "no projection" — never "no normalisation".
144
+
145
+ `viewResult` minifies `compact` and `full` and leaves `raw` indented (that rung
146
+ exists to be read by a person); `minifiedResult` is the same rule with no view
147
+ to hand. Formatting whitespace only — whitespace *inside* a value is content
148
+ and is never touched.
149
+
108
150
  ### `errors` — helpful errors
109
151
 
110
152
  `McpToolError` and its subclasses (`SessionNotAuthenticatedError`,
@@ -48,4 +48,5 @@ export declare function pruneUndefined<T extends Record<string, unknown>>(obj: T
48
48
  * rather than a 1-element array.
49
49
  */
50
50
  export declare function toArray<T>(value: T | T[] | null | undefined): T[];
51
+ export * from './view.js';
51
52
  //# 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"}
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"}
@@ -115,4 +115,5 @@ export function toArray(value) {
115
115
  return [];
116
116
  return Array.isArray(value) ? value : [value];
117
117
  }
118
+ export * from './view.js';
118
119
  //# 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,123 @@
1
+ import { z } from 'zod';
2
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3
+ /**
4
+ * The fleet's response-shape vocabulary (`docs/fleet-conventions.md`,
5
+ * "Response shape").
6
+ *
7
+ * Every tool that reads an upstream API answers in one of three shapes, and
8
+ * the CHEAP one is the default. That inversion is the whole point: five repos
9
+ * had already grown a projection by the time this was written, and four of
10
+ * them made it opt-in — `compact: false` in the schema, with the tool's own
11
+ * description asking the model to please pass `compact=true`. An efficiency
12
+ * that has to be requested is one that is usually not, and the caller paying
13
+ * for it is the one least able to know it was available.
14
+ *
15
+ * The rungs, in ascending order of size:
16
+ *
17
+ * - **compact** — the projection: the fields a caller acts on, with anything
18
+ * the response already carries elsewhere removed. The default.
19
+ * - **full** — every field this MCP understands, nothing dropped.
20
+ * - **raw** — the upstream payload, unprojected.
21
+ *
22
+ * `raw` means "no projection". It NEVER means "no normalisation": a repo that
23
+ * rewrites values on the way out — timestamps into a single zone, ids into a
24
+ * stable shape — keeps doing so on every rung. Handing back the untouched wire
25
+ * format would reintroduce exactly the inconsistencies that normalisation
26
+ * exists to remove, on the one rung a caller reaches for when something
27
+ * already looks wrong.
28
+ *
29
+ * A tool registers only the rungs it can honour, so a schema never advertises
30
+ * a value that would silently alias to another: `raw` is meaningless where a
31
+ * record is ASSEMBLED from several endpoints rather than passed through from
32
+ * one, and those tools offer `compact` and `full` alone.
33
+ */
34
+ export declare const VIEWS: readonly ["compact", "full", "raw"];
35
+ /** One of {@link VIEWS}: the shape a read tool answers in. */
36
+ export type View = (typeof VIEWS)[number];
37
+ /** Efficiency is not something a caller should have to ask for. */
38
+ export declare const DEFAULT_VIEW: View;
39
+ /** Per-tool tuning for {@link viewParam}. */
40
+ export interface ViewParamOptions {
41
+ /**
42
+ * What THIS tool's compact rung leaves out, in the tool's own words.
43
+ *
44
+ * Worth writing. The generic blurb says a projection happened; only the tool
45
+ * can say which field a caller who needs it should ask for `full` to get.
46
+ */
47
+ note?: string;
48
+ }
49
+ /**
50
+ * The `view` parameter, built from the rungs a tool actually honours.
51
+ *
52
+ * `view` rather than `detail`, `mode` or `compact`: `detail` is already an
53
+ * UPSTREAM passthrough in alltrails-mcp (`basic|medium|offline` goes into the
54
+ * URL), `mode` and `format` are taken elsewhere in the fleet, and `compact` as
55
+ * a boolean cannot express three rungs — nor say whether `compact: false`
56
+ * meant "everything you understand" or "everything you received".
57
+ */
58
+ export declare function viewParam(honoured: readonly View[], opts?: ViewParamOptions): z.ZodOptional<z.ZodEnum<Record<string, string>>>;
59
+ /**
60
+ * The rung to answer in. Absent means `compact`.
61
+ *
62
+ * The schema has already rejected anything this tool does not honour, so this
63
+ * is the second line — and it fails toward the CHEAP answer rather than
64
+ * throwing, because a caller that somehow named an unavailable rung is better
65
+ * served by a small correct response than by an error.
66
+ */
67
+ export declare function resolveView(value: string | undefined, honoured: readonly View[]): View;
68
+ /**
69
+ * A tool result with no formatting whitespace.
70
+ *
71
+ * `JSON.stringify(data, null, 2)` — the fleet's most duplicated line — spends
72
+ * roughly a fifth of a large response on indentation that carries no
73
+ * information and that nothing downstream reads: measured at 23% of a
74
+ * 135 KB ofw-mcp message page, or about 8,000 tokens per call.
75
+ *
76
+ * Only FORMATTING whitespace goes. Whitespace inside a value is content — the
77
+ * blank line between paragraphs of a message body, the indentation of a quoted
78
+ * block — and `JSON.stringify` leaves every byte of it alone, because it drops
79
+ * only the indent and the runs after `:` and `,`. Any hand-rolled alternative
80
+ * (a regex over the serialised text, a collapse of `\s+`) corrupts exactly the
81
+ * payloads this is meant to shrink. There are tests on that; do not replace
82
+ * this with something cleverer.
83
+ *
84
+ * Key ORDER is untouched too, which several repos depend on (ofw-mcp emits its
85
+ * paging state before its data array precisely so a truncated read still sees
86
+ * it).
87
+ */
88
+ export declare function minifiedResult(data: unknown): CallToolResult;
89
+ /**
90
+ * A tool result whose whitespace matches its rung: `compact` and `full` are
91
+ * minified, `raw` stays indented.
92
+ *
93
+ * The asymmetry is deliberate rather than an oversight. `compact` and `full`
94
+ * are consumed by a model, which reads the parse and not the layout. `raw` is
95
+ * the rung a person reaches for when a payload is not what they expected, and
96
+ * indentation is most of what makes an unfamiliar shape legible.
97
+ */
98
+ export declare function viewResult(view: View, data: unknown): CallToolResult;
99
+ /** Who is projecting what, for the stderr line {@link projectOrRaw} writes on a fallback. */
100
+ export interface ProjectOptions {
101
+ /** The MCP's own name, for the stderr line. */
102
+ label: string;
103
+ /** Which call produced the value — the endpoint, usually. */
104
+ context: string;
105
+ }
106
+ /**
107
+ * Project a value, falling back to the RAW value if the projection fails.
108
+ *
109
+ * This is what makes compact-by-default survivable on a reverse-engineered
110
+ * API, and it is not optional. A projector names fields; upstream changes
111
+ * them; and the two failure modes that follow are an exception (loud, but it
112
+ * takes the whole tool call down) and an empty or half-filled record (silent,
113
+ * and indistinguishable from "there was nothing there") — which is the same
114
+ * false negative every absence-reporting guard in this fleet exists to
115
+ * prevent. So a projector that throws, or that returns nothing, hands back
116
+ * everything instead and says why on stderr.
117
+ *
118
+ * `undefined` counts as a failure. A projection legitimately has nothing to
119
+ * say only about a value that was already absent, and a caller cannot tell
120
+ * that from a projector that lost its footing.
121
+ */
122
+ export declare function projectOrRaw<T, R>(value: T, project: (value: T) => R, opts: ProjectOptions): T | R;
123
+ //# sourceMappingURL=view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/response/view.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,KAAK,qCAAsC,CAAC;AAEzD,8DAA8D;AAC9D,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1C,mEAAmE;AACnE,eAAO,MAAM,YAAY,EAAE,IAAgB,CAAC;AAQ5C,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,SAAS,IAAI,EAAE,EAAE,IAAI,GAAE,gBAAqB,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAmBlI;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,SAAS,IAAI,EAAE,GAAG,IAAI,CAEtF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAE5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,cAAc,CAEpE;AAED,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,cAAc,GAAG,CAAC,GAAG,CAAC,CAYlG"}
@@ -0,0 +1,150 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The fleet's response-shape vocabulary (`docs/fleet-conventions.md`,
4
+ * "Response shape").
5
+ *
6
+ * Every tool that reads an upstream API answers in one of three shapes, and
7
+ * the CHEAP one is the default. That inversion is the whole point: five repos
8
+ * had already grown a projection by the time this was written, and four of
9
+ * them made it opt-in — `compact: false` in the schema, with the tool's own
10
+ * description asking the model to please pass `compact=true`. An efficiency
11
+ * that has to be requested is one that is usually not, and the caller paying
12
+ * for it is the one least able to know it was available.
13
+ *
14
+ * The rungs, in ascending order of size:
15
+ *
16
+ * - **compact** — the projection: the fields a caller acts on, with anything
17
+ * the response already carries elsewhere removed. The default.
18
+ * - **full** — every field this MCP understands, nothing dropped.
19
+ * - **raw** — the upstream payload, unprojected.
20
+ *
21
+ * `raw` means "no projection". It NEVER means "no normalisation": a repo that
22
+ * rewrites values on the way out — timestamps into a single zone, ids into a
23
+ * stable shape — keeps doing so on every rung. Handing back the untouched wire
24
+ * format would reintroduce exactly the inconsistencies that normalisation
25
+ * exists to remove, on the one rung a caller reaches for when something
26
+ * already looks wrong.
27
+ *
28
+ * A tool registers only the rungs it can honour, so a schema never advertises
29
+ * a value that would silently alias to another: `raw` is meaningless where a
30
+ * record is ASSEMBLED from several endpoints rather than passed through from
31
+ * one, and those tools offer `compact` and `full` alone.
32
+ */
33
+ export const VIEWS = ['compact', 'full', 'raw'];
34
+ /** Efficiency is not something a caller should have to ask for. */
35
+ export const DEFAULT_VIEW = 'compact';
36
+ const BLURB = {
37
+ compact: '"compact" (default) drops fields the response already carries elsewhere',
38
+ full: '"full" returns every field this server understands',
39
+ raw: '"raw" returns the upstream payload unprojected',
40
+ };
41
+ /**
42
+ * The `view` parameter, built from the rungs a tool actually honours.
43
+ *
44
+ * `view` rather than `detail`, `mode` or `compact`: `detail` is already an
45
+ * UPSTREAM passthrough in alltrails-mcp (`basic|medium|offline` goes into the
46
+ * URL), `mode` and `format` are taken elsewhere in the fleet, and `compact` as
47
+ * a boolean cannot express three rungs — nor say whether `compact: false`
48
+ * meant "everything you understand" or "everything you received".
49
+ */
50
+ export function viewParam(honoured, opts = {}) {
51
+ if (honoured.length < 2) {
52
+ throw new Error('viewParam needs at least two rungs: a parameter offering one value decides nothing');
53
+ }
54
+ if (!honoured.includes('compact')) {
55
+ // A tool whose rungs are `full` and `raw` has no cheap answer at all,
56
+ // which is the shape this vocabulary exists to remove — not one it should
57
+ // help express.
58
+ throw new Error('viewParam must offer "compact": a tool with no cheap rung has nothing to default to');
59
+ }
60
+ const ordered = VIEWS.filter((v) => honoured.includes(v));
61
+ const sentence = `Response shape: ${ordered.map((v) => BLURB[v]).join('; ')}.`;
62
+ // `.describe()` LAST, after `.optional()`. Applied to the enum it lands on
63
+ // the inner type, and the wrapper an MCP host actually reads its description
64
+ // off comes back blank — a parameter documented to nobody.
65
+ return z
66
+ .enum(Object.fromEntries(ordered.map((v) => [v, v])))
67
+ .optional()
68
+ .describe(opts.note ? `${sentence} ${opts.note}` : sentence);
69
+ }
70
+ /**
71
+ * The rung to answer in. Absent means `compact`.
72
+ *
73
+ * The schema has already rejected anything this tool does not honour, so this
74
+ * is the second line — and it fails toward the CHEAP answer rather than
75
+ * throwing, because a caller that somehow named an unavailable rung is better
76
+ * served by a small correct response than by an error.
77
+ */
78
+ export function resolveView(value, honoured) {
79
+ return value !== undefined && honoured.includes(value) ? value : DEFAULT_VIEW;
80
+ }
81
+ /**
82
+ * A tool result with no formatting whitespace.
83
+ *
84
+ * `JSON.stringify(data, null, 2)` — the fleet's most duplicated line — spends
85
+ * roughly a fifth of a large response on indentation that carries no
86
+ * information and that nothing downstream reads: measured at 23% of a
87
+ * 135 KB ofw-mcp message page, or about 8,000 tokens per call.
88
+ *
89
+ * Only FORMATTING whitespace goes. Whitespace inside a value is content — the
90
+ * blank line between paragraphs of a message body, the indentation of a quoted
91
+ * block — and `JSON.stringify` leaves every byte of it alone, because it drops
92
+ * only the indent and the runs after `:` and `,`. Any hand-rolled alternative
93
+ * (a regex over the serialised text, a collapse of `\s+`) corrupts exactly the
94
+ * payloads this is meant to shrink. There are tests on that; do not replace
95
+ * this with something cleverer.
96
+ *
97
+ * Key ORDER is untouched too, which several repos depend on (ofw-mcp emits its
98
+ * paging state before its data array precisely so a truncated read still sees
99
+ * it).
100
+ */
101
+ export function minifiedResult(data) {
102
+ return { content: [{ type: 'text', text: JSON.stringify(data) }] };
103
+ }
104
+ /**
105
+ * A tool result whose whitespace matches its rung: `compact` and `full` are
106
+ * minified, `raw` stays indented.
107
+ *
108
+ * The asymmetry is deliberate rather than an oversight. `compact` and `full`
109
+ * are consumed by a model, which reads the parse and not the layout. `raw` is
110
+ * the rung a person reaches for when a payload is not what they expected, and
111
+ * indentation is most of what makes an unfamiliar shape legible.
112
+ */
113
+ export function viewResult(view, data) {
114
+ return view === 'raw' ? { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] } : minifiedResult(data);
115
+ }
116
+ /**
117
+ * Project a value, falling back to the RAW value if the projection fails.
118
+ *
119
+ * This is what makes compact-by-default survivable on a reverse-engineered
120
+ * API, and it is not optional. A projector names fields; upstream changes
121
+ * them; and the two failure modes that follow are an exception (loud, but it
122
+ * takes the whole tool call down) and an empty or half-filled record (silent,
123
+ * and indistinguishable from "there was nothing there") — which is the same
124
+ * false negative every absence-reporting guard in this fleet exists to
125
+ * prevent. So a projector that throws, or that returns nothing, hands back
126
+ * everything instead and says why on stderr.
127
+ *
128
+ * `undefined` counts as a failure. A projection legitimately has nothing to
129
+ * say only about a value that was already absent, and a caller cannot tell
130
+ * that from a projector that lost its footing.
131
+ */
132
+ export function projectOrRaw(value, project, opts) {
133
+ try {
134
+ const projected = project(value);
135
+ if (projected === undefined) {
136
+ warn(opts, 'the projection produced nothing');
137
+ return value;
138
+ }
139
+ return projected;
140
+ }
141
+ catch (error) {
142
+ warn(opts, error instanceof Error ? error.message : String(error));
143
+ return value;
144
+ }
145
+ }
146
+ function warn(opts, why) {
147
+ console.error(`[${opts.label}] WARNING: could not project ${opts.context} (${why}); returning the unprojected payload. ` +
148
+ 'The upstream shape has probably changed — the projection needs updating.');
149
+ }
150
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/response/view.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.20.0",
3
+ "version": "0.21.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",