@clien-ai/mcp 0.2.1 → 0.4.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.
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Backward-compatible camelCase → snake_case parameter aliasing (FUL-233).
3
+ *
4
+ * The tool surface settled on snake_case params (`project_id`, `persona_id`,
5
+ * `job_id`, …) as every read tool landed, but five older write tools still
6
+ * advertised camelCase (`projectId`, `companySize`, `groundInEvidence`, …).
7
+ * A public API cannot simply rename those: `@clien-ai/mcp` is published to npm
8
+ * and existing callers pass the old spellings. So each legacy name survives as
9
+ * a DEPRECATED ALIAS while snake_case becomes the documented form.
10
+ *
11
+ * How it fits together — three pieces, one source of truth per tool:
12
+ *
13
+ * 1. The tool's Zod schema declares the canonical snake_case field normally,
14
+ * plus the camelCase alias via `deprecatedAlias()`. Declaring the alias is
15
+ * load-bearing, not cosmetic: `zodToInputSchema` stamps
16
+ * `additionalProperties: false` on every advert, so an UNDECLARED alias
17
+ * would be a field the schema publicly disowns while the server quietly
18
+ * honours it — and strict MCP clients would drop it before it ever reached
19
+ * us. Declared, the advert and the runtime tell the same story.
20
+ * 2. The registry entry carries the matching `paramAliases` map, and
21
+ * `defineTool` wraps the handler with `normalizeParamAliases` — so a tool
22
+ * cannot forget to call it, and handlers only ever see canonical keys.
23
+ * 3. `__tests__/param-aliases.test.ts` binds (1) to (2): every camelCase
24
+ * property advertised by any registry tool must appear in that tool's
25
+ * alias map, and vice versa. Adding one without the other fails there
26
+ * rather than silently advertising a param nothing honours.
27
+ *
28
+ * Normalization runs BEFORE `safeParse`, which is why the alias's declared Zod
29
+ * type never actually validates anything — by parse time the key is gone,
30
+ * rewritten onto its canonical name. It exists purely so the advert is
31
+ * truthful. It still mirrors the canonical validator so a client reading the
32
+ * advert is not misled about what the alias accepts.
33
+ *
34
+ * Deliberately NOT a Zod `.refine()`/`.transform()`: those produce a
35
+ * `ZodEffects`, which has no `.shape`, and both `zodToInputSchema` and
36
+ * `__tests__/schema-contract.test.ts` depend on `.shape` (see the same note in
37
+ * `interview.ts` and `personas.ts`). Cross-field rules live in plain code.
38
+ *
39
+ * ── SCOPE OF THE COMPATIBILITY GUARANTEE ────────────────────────────────────
40
+ * An alias is honoured at `tools/call`, AND an alias-only payload is valid
41
+ * against the advertised schema. `required` no longer excludes alias-only calls.
42
+ *
43
+ * That second half was not free (FUL-238). An alias is a SEPARATE optional
44
+ * property, so it can never satisfy a `required` entry naming the canonical
45
+ * field — the two are different keys. The only way to make the advert accept
46
+ * what the runtime accepts was to drop the canonical id from `required`
47
+ * entirely: the three aliased ids (`create_persona` / `generate_persona`
48
+ * `project_id`, `enrich_persona` `persona_id`) are declared `.optional()` and
49
+ * their presence is enforced in the handler by `requireId` in `personas.ts` —
50
+ * the convention `interview_persona` (per-action ids) and `update_persona`
51
+ * (at-least-one-field) already use. `interview_persona`'s three ids were always
52
+ * this way, so they never had the gap.
53
+ *
54
+ * The trade, taken deliberately: every caller loses the advert's machine-readable
55
+ * "this field is mandatory" signal for those ids, so each one says REQUIRED in
56
+ * its `.describe()` prose and a missing id fails with a clear handler error
57
+ * naming the canonical param — before any HTTP call. In exchange, no client can
58
+ * refuse a legacy caller's payload before sending it.
59
+ *
60
+ * `__tests__/param-aliases.test.ts` pins BOTH halves — the advert no longer
61
+ * listing those ids in `required`, and an alias-only call still reaching the
62
+ * real request — so neither can drift silently.
63
+ */
64
+ import { ToolError } from './errors.js';
65
+ /**
66
+ * Declare a deprecated camelCase alias property for a tool's Zod schema.
67
+ *
68
+ * Pass the canonical field's BASE validator (before `.optional()`): the alias
69
+ * is always optional regardless of whether the canonical field is required, so
70
+ * a caller may supply either one.
71
+ */
72
+ export function deprecatedAlias(canonical, schema) {
73
+ return schema
74
+ .optional()
75
+ .describe(`DEPRECATED alias for \`${canonical}\` — pass \`${canonical}\` instead. ` +
76
+ 'Still accepted for backward compatibility. Passing both this and ' +
77
+ `\`${canonical}\` with DIFFERENT values is rejected.`);
78
+ }
79
+ /** A key counts as supplied when present and not `undefined` (explicit `null` counts). */
80
+ function isSupplied(source, key) {
81
+ return key in source && source[key] !== undefined;
82
+ }
83
+ /**
84
+ * Whether two supplied values are interchangeable. Aliased params are strings
85
+ * and booleans today; the JSON fallback keeps the check correct if one ever
86
+ * becomes structured, and never throws (both values came off a JSON payload).
87
+ */
88
+ function sameValue(a, b) {
89
+ if (Object.is(a, b))
90
+ return true;
91
+ if (typeof a !== typeof b)
92
+ return false;
93
+ if (typeof a !== 'object')
94
+ return false;
95
+ return JSON.stringify(a) === JSON.stringify(b);
96
+ }
97
+ /**
98
+ * Rewrite any supplied camelCase aliases onto their canonical snake_case names.
99
+ *
100
+ * Returns the input untouched (same reference) when no alias is present, so the
101
+ * overwhelmingly common modern call path allocates nothing. Non-object input is
102
+ * passed straight through — the tool's own `safeParse` owns that error message.
103
+ *
104
+ * Supplying BOTH spellings of one field is only an error when the values
105
+ * DIFFER: a caller that sends the same value twice is unambiguous, and failing
106
+ * it would break a belt-and-braces caller for no safety gain. Differing values
107
+ * are genuinely ambiguous, so we refuse rather than pick a winner — silently
108
+ * preferring one would send a persona edit to whichever project the caller
109
+ * did NOT mean.
110
+ *
111
+ * @throws {ToolError} when one field is supplied under both names with
112
+ * different values.
113
+ */
114
+ export function normalizeParamAliases(input, aliases) {
115
+ if (input === null || typeof input !== 'object' || Array.isArray(input))
116
+ return input;
117
+ const source = input;
118
+ const supplied = Object.keys(aliases).filter((alias) => isSupplied(source, alias));
119
+ if (supplied.length === 0)
120
+ return input;
121
+ const out = { ...source };
122
+ for (const alias of supplied) {
123
+ const canonical = aliases[alias];
124
+ const aliasValue = source[alias];
125
+ const canonicalSupplied = isSupplied(source, canonical);
126
+ // A `null` alias alongside a real canonical value is a JSON-shaped "unset",
127
+ // not a competing answer — dropping it is unambiguous, so refusing the call
128
+ // would fail a caller who is not actually contradicting themselves. Left
129
+ // alone when the alias is the ONLY spelling supplied, so the canonical
130
+ // field's own Zod error ("expected string, received null") still surfaces.
131
+ if (aliasValue === null && canonicalSupplied && source[canonical] !== null) {
132
+ delete out[alias];
133
+ continue;
134
+ }
135
+ if (canonicalSupplied && !sameValue(source[canonical], aliasValue)) {
136
+ throw new ToolError(`Invalid input — \`${canonical}\` and its deprecated alias \`${alias}\` were both ` +
137
+ `provided with different values. Pass only \`${canonical}\`.`);
138
+ }
139
+ out[canonical] = aliasValue;
140
+ delete out[alias];
141
+ }
142
+ return out;
143
+ }
144
+ //# sourceMappingURL=param-aliases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"param-aliases.js","sourceRoot":"","sources":["../../src/tools/param-aliases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAKvC;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,SAAiB,EACjB,MAAoB;IAEpB,OAAO,MAAM;SACV,QAAQ,EAAE;SACV,QAAQ,CACP,0BAA0B,SAAS,eAAe,SAAS,cAAc;QACvE,mEAAmE;QACnE,KAAK,SAAS,uCAAuC,CACxD,CAAA;AACL,CAAC;AAED,0FAA0F;AAC1F,SAAS,UAAU,CAAC,MAA+B,EAAE,GAAW;IAC9D,OAAO,GAAG,IAAI,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,SAAS,CAAA;AACnD,CAAC;AAED;;;;GAIG;AACH,SAAS,SAAS,CAAC,CAAU,EAAE,CAAU;IACvC,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IAChC,IAAI,OAAO,CAAC,KAAK,OAAO,CAAC;QAAE,OAAO,KAAK,CAAA;IACvC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACvC,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAChD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAc,EAAE,OAAiB;IACrE,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAErF,MAAM,MAAM,GAAG,KAAgC,CAAA;IAC/C,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;IAClF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAEvC,MAAM,GAAG,GAAG,EAAE,GAAG,MAAM,EAAE,CAAA;IACzB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAE,CAAA;QACjC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAChC,MAAM,iBAAiB,GAAG,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;QAEvD,4EAA4E;QAC5E,4EAA4E;QAC5E,yEAAyE;QACzE,uEAAuE;QACvE,2EAA2E;QAC3E,IAAI,UAAU,KAAK,IAAI,IAAI,iBAAiB,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC;YAC3E,OAAO,GAAG,CAAC,KAAK,CAAC,CAAA;YACjB,SAAQ;QACV,CAAC;QAED,IAAI,iBAAiB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;YACnE,MAAM,IAAI,SAAS,CACjB,qBAAqB,SAAS,iCAAiC,KAAK,eAAe;gBACjF,+CAA+C,SAAS,KAAK,CAChE,CAAA;QACH,CAAC;QACD,GAAG,CAAC,SAAS,CAAC,GAAG,UAAU,CAAA;QAC3B,OAAO,GAAG,CAAC,KAAK,CAAC,CAAA;IACnB,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}