@contractkit/plugin-typescript 0.28.0 → 0.28.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.
Files changed (51) hide show
  1. package/.turbo/turbo-build$colon$ci.log +5 -5
  2. package/.turbo/turbo-test$colon$ci.log +22 -19
  3. package/CHANGELOG.md +14 -0
  4. package/dist/codegen-contract.d.ts.map +1 -1
  5. package/dist/codegen-mcp.d.ts +38 -0
  6. package/dist/codegen-mcp.d.ts.map +1 -0
  7. package/dist/codegen-operation.d.ts +42 -1
  8. package/dist/codegen-operation.d.ts.map +1 -1
  9. package/dist/codegen-sdk.d.ts.map +1 -1
  10. package/dist/index.d.ts +27 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +807 -243
  13. package/dist/index.js.map +1 -1
  14. package/dist/path-utils.d.ts.map +1 -1
  15. package/dist/ts-render.d.ts +6 -0
  16. package/dist/ts-render.d.ts.map +1 -1
  17. package/package.json +2 -2
  18. package/src/codegen-contract.ts +7 -4
  19. package/src/codegen-mcp.ts +501 -0
  20. package/src/codegen-operation.ts +43 -9
  21. package/src/codegen-plain-types.ts +17 -14
  22. package/src/codegen-sdk.ts +5 -4
  23. package/src/index.ts +154 -0
  24. package/src/path-utils.ts +37 -20
  25. package/src/ts-render.ts +21 -6
  26. package/tests/codegen-contract.test.ts +4 -0
  27. package/tests/codegen-mcp.test.ts +246 -0
  28. package/tests/codegen-operation.test.ts +18 -0
  29. package/tests/codegen-sdk.test.ts +8 -0
  30. package/tests/escaping-security.test.ts +143 -0
  31. package/tests/pipeline.test.ts +59 -0
  32. package/coverage/base.css +0 -224
  33. package/coverage/block-navigation.js +0 -87
  34. package/coverage/clover.xml +0 -2213
  35. package/coverage/coverage-final.json +0 -9
  36. package/coverage/favicon.png +0 -0
  37. package/coverage/index.html +0 -131
  38. package/coverage/prettify.css +0 -1
  39. package/coverage/prettify.js +0 -2
  40. package/coverage/sort-arrow-sprite.png +0 -0
  41. package/coverage/sorter.js +0 -210
  42. package/coverage/src/codegen-contract.ts.html +0 -3661
  43. package/coverage/src/codegen-operation.ts.html +0 -2584
  44. package/coverage/src/codegen-plain-types.ts.html +0 -997
  45. package/coverage/src/codegen-sdk.ts.html +0 -3901
  46. package/coverage/src/index.html +0 -206
  47. package/coverage/src/index.ts.html +0 -2761
  48. package/coverage/src/path-utils.ts.html +0 -745
  49. package/coverage/src/ts-render.ts.html +0 -592
  50. package/coverage/tests/helpers.ts.html +0 -826
  51. package/coverage/tests/index.html +0 -116
package/dist/index.js CHANGED
@@ -2,12 +2,166 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/index.ts
5
- import { resolve as resolve2, join as join2, relative as relative6, dirname as dirname6, basename as basename3 } from "path";
5
+ import { resolve as resolve2, join as join2, relative as relative7, dirname as dirname7, basename as basename4 } from "path";
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync, rmdirSync } from "fs";
7
7
 
8
8
  // src/codegen-contract.ts
9
9
  import { relative, dirname } from "path";
10
10
  import { collectTypeRefs, computeModelsWithOutput as ckComputeModelsWithOutput, collectExternalOutputRefs as ckCollectExternalOutputRefs } from "@contractkit/core";
11
+
12
+ // src/ts-render.ts
13
+ var JSON_VALUE_TYPE_DECL = "export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };";
14
+ function quoteKey(name) {
15
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name}'`;
16
+ }
17
+ __name(quoteKey, "quoteKey");
18
+ function escapeJsDocLines(text) {
19
+ return text.replace(/\*\//g, "*\\/").split("\n");
20
+ }
21
+ __name(escapeJsDocLines, "escapeJsDocLines");
22
+ function escapeSingleQuoted(s) {
23
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
24
+ }
25
+ __name(escapeSingleQuoted, "escapeSingleQuoted");
26
+ function headerNameToProperty(name) {
27
+ const parts = name.split(/[-_]/).filter(Boolean);
28
+ return parts.map((p, i) => {
29
+ const lower = p.toLowerCase();
30
+ return i === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
31
+ }).join("");
32
+ }
33
+ __name(headerNameToProperty, "headerNameToProperty");
34
+ function renderTsType(type) {
35
+ switch (type.kind) {
36
+ case "scalar":
37
+ return renderTsScalar(type.name);
38
+ case "array": {
39
+ const inner = renderTsType(type.item);
40
+ const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
41
+ return needsParens ? `(${inner})[]` : `${inner}[]`;
42
+ }
43
+ case "tuple":
44
+ return `[${type.items.map(renderTsType).join(", ")}]`;
45
+ case "record":
46
+ return `Record<${renderTsType(type.key)}, ${renderTsType(type.value)}>`;
47
+ case "enum":
48
+ return type.values.map((v) => `'${escapeSingleQuoted(v)}'`).join(" | ");
49
+ case "literal":
50
+ return typeof type.value === "string" ? `'${escapeSingleQuoted(type.value)}'` : String(type.value);
51
+ case "union":
52
+ return type.members.map(renderTsType).join(" | ");
53
+ case "discriminatedUnion":
54
+ return type.members.map(renderTsType).join(" | ");
55
+ case "intersection":
56
+ return type.members.map(renderTsType).join(" & ");
57
+ case "ref":
58
+ return type.name;
59
+ case "lazy":
60
+ return renderTsType(type.inner);
61
+ case "inlineObject":
62
+ return renderTsInlineObject(type.fields);
63
+ default:
64
+ return "unknown";
65
+ }
66
+ }
67
+ __name(renderTsType, "renderTsType");
68
+ function renderTsScalar(name) {
69
+ switch (name) {
70
+ case "string":
71
+ case "email":
72
+ case "url":
73
+ case "uuid":
74
+ return "string";
75
+ case "number":
76
+ case "int":
77
+ return "number";
78
+ case "bigint":
79
+ return "bigint";
80
+ case "boolean":
81
+ return "boolean";
82
+ case "date":
83
+ case "time":
84
+ case "datetime":
85
+ case "duration":
86
+ case "interval":
87
+ return "string";
88
+ case "null":
89
+ return "null";
90
+ case "unknown":
91
+ return "unknown";
92
+ case "object":
93
+ return "Record<string, unknown>";
94
+ case "binary":
95
+ return "Blob";
96
+ case "json":
97
+ return "JsonValue";
98
+ default: {
99
+ const _exhaustive = name;
100
+ throw new Error(`plugin-typescript: unmapped scalar '${String(_exhaustive)}' \u2014 add a case`);
101
+ }
102
+ }
103
+ }
104
+ __name(renderTsScalar, "renderTsScalar");
105
+ function renderTsInlineObject(fields) {
106
+ const entries = fields.map((f) => {
107
+ const opt = f.optional ? "?" : "";
108
+ return `${quoteKey(f.name)}${opt}: ${renderTsType(f.type)}`;
109
+ });
110
+ return `{ ${entries.join("; ")} }`;
111
+ }
112
+ __name(renderTsInlineObject, "renderTsInlineObject");
113
+ function renderInputTsType(type, modelsWithInput) {
114
+ if (!modelsWithInput || modelsWithInput.size === 0) return renderTsType(type);
115
+ switch (type.kind) {
116
+ case "ref":
117
+ return modelsWithInput.has(type.name) ? `${type.name}Input` : type.name;
118
+ case "array": {
119
+ const inner = renderInputTsType(type.item, modelsWithInput);
120
+ const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
121
+ return needsParens ? `(${inner})[]` : `${inner}[]`;
122
+ }
123
+ case "intersection":
124
+ return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" & ");
125
+ case "union":
126
+ return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" | ");
127
+ case "discriminatedUnion":
128
+ return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" | ");
129
+ case "inlineObject":
130
+ return `{ ${type.fields.map((f) => `${quoteKey(f.name)}${f.optional ? "?" : ""}: ${renderInputTsType(f.type, modelsWithInput)}`).join("; ")} }`;
131
+ case "lazy":
132
+ return renderInputTsType(type.inner, modelsWithInput);
133
+ default:
134
+ return renderTsType(type);
135
+ }
136
+ }
137
+ __name(renderInputTsType, "renderInputTsType");
138
+ function renderOutputTsType(type, modelsWithOutput) {
139
+ if (!modelsWithOutput || modelsWithOutput.size === 0) return renderTsType(type);
140
+ switch (type.kind) {
141
+ case "ref":
142
+ return modelsWithOutput.has(type.name) ? `${type.name}Output` : type.name;
143
+ case "array": {
144
+ const inner = renderOutputTsType(type.item, modelsWithOutput);
145
+ const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
146
+ return needsParens ? `(${inner})[]` : `${inner}[]`;
147
+ }
148
+ case "intersection":
149
+ return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" & ");
150
+ case "union":
151
+ return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" | ");
152
+ case "discriminatedUnion":
153
+ return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" | ");
154
+ case "inlineObject":
155
+ return `{ ${type.fields.map((f) => `${quoteKey(f.name)}${f.optional ? "?" : ""}: ${renderOutputTsType(f.type, modelsWithOutput)}`).join("; ")} }`;
156
+ case "lazy":
157
+ return renderOutputTsType(type.inner, modelsWithOutput);
158
+ default:
159
+ return renderTsType(type);
160
+ }
161
+ }
162
+ __name(renderOutputTsType, "renderOutputTsType");
163
+
164
+ // src/codegen-contract.ts
11
165
  function modeToWrapper(mode) {
12
166
  switch (mode) {
13
167
  case "strict":
@@ -56,7 +210,7 @@ function generateComments(model, outPath) {
56
210
  lines.push(` * @deprecated`);
57
211
  }
58
212
  if (model.description) {
59
- lines.push(` * ${model.description}`);
213
+ for (const l of escapeJsDocLines(model.description)) lines.push(` * ${l}`);
60
214
  }
61
215
  const relPath = outPath ? relative(dirname(outPath), model.loc.file) : model.loc.file;
62
216
  lines.push(` * generated from [${model.name}](file://./${relPath}#L${model.loc.line})`);
@@ -199,9 +353,9 @@ function generateSimpleModel(model, outPath) {
199
353
  const outputKey = applyCase(field.name, outputCase);
200
354
  if (field.optional) {
201
355
  const guard = hasInputTransform ? `data.${inputKey} != null` : `data.${inputKey} !== undefined`;
202
- lines.push(` ...(${guard} ? { ${quoteKey(outputKey)}: data.${inputKey} } : {}),`);
356
+ lines.push(` ...(${guard} ? { ${quoteKey2(outputKey)}: data.${inputKey} } : {}),`);
203
357
  } else {
204
- lines.push(` ${quoteKey(outputKey)}: data.${inputKey},`);
358
+ lines.push(` ${quoteKey2(outputKey)}: data.${inputKey},`);
205
359
  }
206
360
  }
207
361
  lines.push(`}));`);
@@ -298,7 +452,7 @@ function generateThreeSchemaModel(model, outPath, modelsWithInput, modelsWithWri
298
452
  }
299
453
  const omitClause = fieldsToOmit.size > 0 ? `.omit({ ${[
300
454
  ...fieldsToOmit
301
- ].map((f) => `${quoteKey(f)}: true`).join(", ")} })` : "";
455
+ ].map((f) => `${quoteKey2(f)}: true`).join(", ")} })` : "";
302
456
  if (bases.length > 0) {
303
457
  const { head, tail } = buildExtendChain(bases, (b) => modelsWithInput?.has(b) ? `${b}Input` : b);
304
458
  lines.push(`export const ${name}Input = ${head}${tail}${omitClause}.extend({`);
@@ -343,7 +497,7 @@ function renderFieldsAsPascalCase(fields, defaultMode) {
343
497
  expr += ".nullable()";
344
498
  }
345
499
  if (f.description) expr += `.describe("${escapeString(f.description)}")`;
346
- return `${quoteKey(pascalKey)}: ${expr},`;
500
+ return `${quoteKey2(pascalKey)}: ${expr},`;
347
501
  });
348
502
  }
349
503
  __name(renderFieldsAsPascalCase, "renderFieldsAsPascalCase");
@@ -361,7 +515,7 @@ function renderFieldsAsSnakeCase(fields, defaultMode) {
361
515
  expr += ".nullable()";
362
516
  }
363
517
  if (f.description) expr += `.describe("${escapeString(f.description)}")`;
364
- return `${quoteKey(snakeKey)}: ${expr},`;
518
+ return `${quoteKey2(snakeKey)}: ${expr},`;
365
519
  });
366
520
  }
367
521
  __name(renderFieldsAsSnakeCase, "renderFieldsAsSnakeCase");
@@ -377,7 +531,7 @@ function renderField(field, defaultMode) {
377
531
  expr += ".optional()";
378
532
  }
379
533
  if (field.description) expr += `.describe("${escapeString(field.description)}")`;
380
- lines.push(`${quoteKey(field.name)}: ${expr},`);
534
+ lines.push(`${quoteKey2(field.name)}: ${expr},`);
381
535
  return lines;
382
536
  }
383
537
  __name(renderField, "renderField");
@@ -502,8 +656,10 @@ function renderScalar(s) {
502
656
  return "_ZodBinary";
503
657
  case "json":
504
658
  return "_ZodJson";
505
- default:
506
- return "z.unknown()";
659
+ default: {
660
+ const _exhaustive = s.name;
661
+ throw new Error(`plugin-typescript: unmapped scalar '${String(_exhaustive)}' \u2014 add a case`);
662
+ }
507
663
  }
508
664
  }
509
665
  __name(renderScalar, "renderScalar");
@@ -523,7 +679,7 @@ function renderRecord(r) {
523
679
  }
524
680
  __name(renderRecord, "renderRecord");
525
681
  function renderEnum(e) {
526
- const vals = e.values.map((v) => `"${v}"`).join(", ");
682
+ const vals = e.values.map((v) => `"${escapeString(v)}"`).join(", ");
527
683
  return `z.enum([${vals}])`;
528
684
  }
529
685
  __name(renderEnum, "renderEnum");
@@ -572,9 +728,9 @@ function renderInlineObject(o, parseCaseTransform, defaultMode) {
572
728
  const transformEntries = o.fields.map((f) => {
573
729
  const snakeKey = camelToSnake(f.name);
574
730
  if (f.optional) {
575
- return ` ...(data.${snakeKey} != null ? { ${quoteKey(f.name)}: data.${snakeKey} } : {}),`;
731
+ return ` ...(data.${snakeKey} != null ? { ${quoteKey2(f.name)}: data.${snakeKey} } : {}),`;
576
732
  }
577
- return ` ${quoteKey(f.name)}: data.${snakeKey},`;
733
+ return ` ${quoteKey2(f.name)}: data.${snakeKey},`;
578
734
  }).join("\n");
579
735
  return `${wrapper}({
580
736
  ${joined}
@@ -588,9 +744,9 @@ ${transformEntries}
588
744
  const transformEntries = o.fields.map((f) => {
589
745
  const pascalKey = camelToPascal(f.name);
590
746
  if (f.optional) {
591
- return ` ...(data.${pascalKey} != null ? { ${quoteKey(f.name)}: data.${pascalKey} } : {}),`;
747
+ return ` ...(data.${pascalKey} != null ? { ${quoteKey2(f.name)}: data.${pascalKey} } : {}),`;
592
748
  }
593
- return ` ${quoteKey(f.name)}: data.${pascalKey},`;
749
+ return ` ${quoteKey2(f.name)}: data.${pascalKey},`;
594
750
  }).join("\n");
595
751
  return `${wrapper}({
596
752
  ${joined}
@@ -676,7 +832,7 @@ function renderInputField(field, modelsWithInput, defaultMode) {
676
832
  expr += ".optional()";
677
833
  }
678
834
  if (field.description) expr += `.describe("${escapeString(field.description)}")`;
679
- lines.push(`${quoteKey(field.name)}: ${expr},`);
835
+ lines.push(`${quoteKey2(field.name)}: ${expr},`);
680
836
  return lines;
681
837
  }
682
838
  __name(renderInputField, "renderInputField");
@@ -736,17 +892,17 @@ function renderQueryField(field, modelsWithInput, defaultMode) {
736
892
  expr += ".optional()";
737
893
  }
738
894
  if (field.description) expr += `.describe("${escapeString(field.description)}")`;
739
- return `${quoteKey(field.name)}: ${expr},`;
895
+ return `${quoteKey2(field.name)}: ${expr},`;
740
896
  }
741
897
  __name(renderQueryField, "renderQueryField");
742
898
  function isValidIdentifier(name) {
743
899
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
744
900
  }
745
901
  __name(isValidIdentifier, "isValidIdentifier");
746
- function quoteKey(name) {
902
+ function quoteKey2(name) {
747
903
  return isValidIdentifier(name) ? name : `'${name}'`;
748
904
  }
749
- __name(quoteKey, "quoteKey");
905
+ __name(quoteKey2, "quoteKey");
750
906
  function escapeString(s) {
751
907
  return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
752
908
  }
@@ -950,149 +1106,6 @@ __name(pascalToDotCase, "pascalToDotCase");
950
1106
 
951
1107
  // src/codegen-operation.ts
952
1108
  import { resolveModifiers, resolveSecurity, SECURITY_NONE, classifyContentType } from "@contractkit/core";
953
-
954
- // src/ts-render.ts
955
- var JSON_VALUE_TYPE_DECL = "export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };";
956
- function quoteKey2(name) {
957
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name}'`;
958
- }
959
- __name(quoteKey2, "quoteKey");
960
- function headerNameToProperty(name) {
961
- const parts = name.split(/[-_]/).filter(Boolean);
962
- return parts.map((p, i) => {
963
- const lower = p.toLowerCase();
964
- return i === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
965
- }).join("");
966
- }
967
- __name(headerNameToProperty, "headerNameToProperty");
968
- function renderTsType(type) {
969
- switch (type.kind) {
970
- case "scalar":
971
- return renderTsScalar(type.name);
972
- case "array": {
973
- const inner = renderTsType(type.item);
974
- const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
975
- return needsParens ? `(${inner})[]` : `${inner}[]`;
976
- }
977
- case "tuple":
978
- return `[${type.items.map(renderTsType).join(", ")}]`;
979
- case "record":
980
- return `Record<${renderTsType(type.key)}, ${renderTsType(type.value)}>`;
981
- case "enum":
982
- return type.values.map((v) => `'${v}'`).join(" | ");
983
- case "literal":
984
- return typeof type.value === "string" ? `'${type.value}'` : String(type.value);
985
- case "union":
986
- return type.members.map(renderTsType).join(" | ");
987
- case "discriminatedUnion":
988
- return type.members.map(renderTsType).join(" | ");
989
- case "intersection":
990
- return type.members.map(renderTsType).join(" & ");
991
- case "ref":
992
- return type.name;
993
- case "lazy":
994
- return renderTsType(type.inner);
995
- case "inlineObject":
996
- return renderTsInlineObject(type.fields);
997
- default:
998
- return "unknown";
999
- }
1000
- }
1001
- __name(renderTsType, "renderTsType");
1002
- function renderTsScalar(name) {
1003
- switch (name) {
1004
- case "string":
1005
- case "email":
1006
- case "url":
1007
- case "uuid":
1008
- return "string";
1009
- case "number":
1010
- case "int":
1011
- return "number";
1012
- case "bigint":
1013
- return "bigint";
1014
- case "boolean":
1015
- return "boolean";
1016
- case "date":
1017
- case "datetime":
1018
- case "duration":
1019
- case "interval":
1020
- return "string";
1021
- case "null":
1022
- return "null";
1023
- case "unknown":
1024
- return "unknown";
1025
- case "object":
1026
- return "Record<string, unknown>";
1027
- case "binary":
1028
- return "Blob";
1029
- case "json":
1030
- return "JsonValue";
1031
- default:
1032
- return "unknown";
1033
- }
1034
- }
1035
- __name(renderTsScalar, "renderTsScalar");
1036
- function renderTsInlineObject(fields) {
1037
- const entries = fields.map((f) => {
1038
- const opt = f.optional ? "?" : "";
1039
- return `${quoteKey2(f.name)}${opt}: ${renderTsType(f.type)}`;
1040
- });
1041
- return `{ ${entries.join("; ")} }`;
1042
- }
1043
- __name(renderTsInlineObject, "renderTsInlineObject");
1044
- function renderInputTsType(type, modelsWithInput) {
1045
- if (!modelsWithInput || modelsWithInput.size === 0) return renderTsType(type);
1046
- switch (type.kind) {
1047
- case "ref":
1048
- return modelsWithInput.has(type.name) ? `${type.name}Input` : type.name;
1049
- case "array": {
1050
- const inner = renderInputTsType(type.item, modelsWithInput);
1051
- const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
1052
- return needsParens ? `(${inner})[]` : `${inner}[]`;
1053
- }
1054
- case "intersection":
1055
- return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" & ");
1056
- case "union":
1057
- return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" | ");
1058
- case "discriminatedUnion":
1059
- return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" | ");
1060
- case "inlineObject":
1061
- return `{ ${type.fields.map((f) => `${quoteKey2(f.name)}${f.optional ? "?" : ""}: ${renderInputTsType(f.type, modelsWithInput)}`).join("; ")} }`;
1062
- case "lazy":
1063
- return renderInputTsType(type.inner, modelsWithInput);
1064
- default:
1065
- return renderTsType(type);
1066
- }
1067
- }
1068
- __name(renderInputTsType, "renderInputTsType");
1069
- function renderOutputTsType(type, modelsWithOutput) {
1070
- if (!modelsWithOutput || modelsWithOutput.size === 0) return renderTsType(type);
1071
- switch (type.kind) {
1072
- case "ref":
1073
- return modelsWithOutput.has(type.name) ? `${type.name}Output` : type.name;
1074
- case "array": {
1075
- const inner = renderOutputTsType(type.item, modelsWithOutput);
1076
- const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
1077
- return needsParens ? `(${inner})[]` : `${inner}[]`;
1078
- }
1079
- case "intersection":
1080
- return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" & ");
1081
- case "union":
1082
- return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" | ");
1083
- case "discriminatedUnion":
1084
- return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" | ");
1085
- case "inlineObject":
1086
- return `{ ${type.fields.map((f) => `${quoteKey2(f.name)}${f.optional ? "?" : ""}: ${renderOutputTsType(f.type, modelsWithOutput)}`).join("; ")} }`;
1087
- case "lazy":
1088
- return renderOutputTsType(type.inner, modelsWithOutput);
1089
- default:
1090
- return renderTsType(type);
1091
- }
1092
- }
1093
- __name(renderOutputTsType, "renderOutputTsType");
1094
-
1095
- // src/codegen-operation.ts
1096
1109
  import { basename, dirname as dirname2, relative as relative2 } from "path";
1097
1110
  function bodyParserToken(contentType) {
1098
1111
  switch (classifyContentType(contentType)) {
@@ -1244,7 +1257,7 @@ function generateHandler(route, op, root, options) {
1244
1257
  lines.push("/**");
1245
1258
  const desc = op.description ?? route.description;
1246
1259
  if (desc) {
1247
- lines.push(` * ${desc}`);
1260
+ for (const l of escapeJsDocLines(desc)) lines.push(` * ${l}`);
1248
1261
  }
1249
1262
  const relFile = outPath ? relative2(dirname2(outPath), file) : file;
1250
1263
  lines.push(` * from [${basename(file)}](file://./${relFile}#L${op.loc.line})`);
@@ -1273,11 +1286,11 @@ function generateHandler(route, op, root, options) {
1273
1286
  middlewares.push(`bodyParserMiddleware([${tokensExpr}])`);
1274
1287
  }
1275
1288
  if (op.signature) {
1276
- const sigArgs = op.signaturePolicy ? `'${op.signature}', { policy: '${op.signaturePolicy}' }` : `'${op.signature}'`;
1289
+ const sigArgs = op.signaturePolicy ? `'${escapeSingleQuoted(op.signature)}', { policy: '${escapeSingleQuoted(op.signaturePolicy)}' }` : `'${escapeSingleQuoted(op.signature)}'`;
1277
1290
  middlewares.push(`requireSignature(${sigArgs})`);
1278
1291
  }
1279
1292
  const middlewareStr = middlewares.length > 0 ? `, ${middlewares.join(", ")},` : ",";
1280
- lines.push(`${deriveRouterName(file)}.${method}('${path}'${middlewareStr} async (ctx, next) => {`);
1293
+ lines.push(`${deriveRouterName(file)}.${method}('${path}'${middlewareStr} async ctx => {`);
1281
1294
  lines.push(...generateParamValidation(route.params, "ctx.params", "params", route.paramsMode ?? "strict", "", modelsWithInput));
1282
1295
  lines.push(...generateParamValidation(op.query, "ctx.query", "query", op.queryMode ?? "strict", "", modelsWithInput));
1283
1296
  lines.push(...generateParamValidation(op.headers, "ctx.headers", "headers", op.headersMode ?? "strip", "", modelsWithInput));
@@ -1308,13 +1321,13 @@ function generateHandler(route, op, root, options) {
1308
1321
  lines.push("");
1309
1322
  }
1310
1323
  }
1311
- const primaryResponse = op.responses.find((r) => r.bodyType) ?? op.responses[0];
1324
+ const primaryResponse2 = op.responses.find((r) => r.bodyType) ?? op.responses[0];
1312
1325
  const serviceParts = inferService(op, route, file);
1313
- const respHeaders = primaryResponse?.headers ?? [];
1326
+ const respHeaders = primaryResponse2?.headers ?? [];
1314
1327
  const hasRespHeaders = respHeaders.length > 0;
1315
- const headersAnnotation = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey2(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, options.modelsWithOutput)}`).join("; ")} }` : "";
1316
- if (primaryResponse?.bodyType) {
1317
- const { annotation, prelude } = formatTypeAnnotation(primaryResponse.bodyType, options.modelsWithOutput);
1328
+ const headersAnnotation = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, options.modelsWithOutput)}`).join("; ")} }` : "";
1329
+ if (primaryResponse2?.bodyType) {
1330
+ const { annotation, prelude } = formatTypeAnnotation(primaryResponse2.bodyType, options.modelsWithOutput);
1318
1331
  if (prelude) {
1319
1332
  lines.push(` ${prelude}`);
1320
1333
  }
@@ -1333,7 +1346,7 @@ function generateHandler(route, op, root, options) {
1333
1346
  }
1334
1347
  }
1335
1348
  lines.push("");
1336
- lines.push(` ctx.status = ${primaryResponse?.statusCode ?? 200};`);
1349
+ lines.push(` ctx.status = ${primaryResponse2?.statusCode ?? 200};`);
1337
1350
  if (hasRespHeaders) {
1338
1351
  for (const h of respHeaders) {
1339
1352
  const accessor = `result.headers[${JSON.stringify(headerNameToProperty(h.name))}]`;
@@ -1344,8 +1357,8 @@ function generateHandler(route, op, root, options) {
1344
1357
  }
1345
1358
  }
1346
1359
  }
1347
- if (primaryResponse?.bodyType && primaryResponse.contentType) {
1348
- lines.push(` ctx.type = '${primaryResponse.contentType}';`);
1360
+ if (primaryResponse2?.bodyType && primaryResponse2.contentType) {
1361
+ lines.push(` ctx.type = '${primaryResponse2.contentType}';`);
1349
1362
  lines.push(` ctx.body = ${hasRespHeaders ? "result.body" : "result"};`);
1350
1363
  }
1351
1364
  lines.push(`});`);
@@ -1947,24 +1960,25 @@ function generateMethod(route, op, file, options) {
1947
1960
  const { modelsWithInput, modelsWithOutput } = options;
1948
1961
  const params = buildMethodParams(route, op, modelsWithInput);
1949
1962
  const paramStr = params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ");
1950
- const primaryResponse = op.responses.find((r) => r.bodyType) ?? op.responses[0];
1951
- const isVoid = !primaryResponse?.bodyType;
1952
- const respCategory = primaryResponse?.contentType ? classifyContentType2(primaryResponse.contentType) : "json";
1953
- const dataType = isVoid ? "void" : respCategory === "text" ? "string" : respCategory === "binary" ? "Blob" : renderOutputTsType(primaryResponse.bodyType, modelsWithOutput);
1954
- const respHeaders = primaryResponse?.headers ?? [];
1963
+ const primaryResponse2 = op.responses.find((r) => r.bodyType) ?? op.responses[0];
1964
+ const isVoid = !primaryResponse2?.bodyType;
1965
+ const respCategory = primaryResponse2?.contentType ? classifyContentType2(primaryResponse2.contentType) : "json";
1966
+ const dataType = isVoid ? "void" : respCategory === "text" ? "string" : respCategory === "binary" ? "Blob" : renderOutputTsType(primaryResponse2.bodyType, modelsWithOutput);
1967
+ const respHeaders = primaryResponse2?.headers ?? [];
1955
1968
  const hasRespHeaders = respHeaders.length > 0;
1956
- const headersShape = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey2(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`).join("; ")} }` : "";
1969
+ const headersShape = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`).join("; ")} }` : "";
1957
1970
  const returnType = hasRespHeaders ? isVoid ? `{ headers: ${headersShape} }` : `{ data: ${dataType}; headers: ${headersShape} }` : dataType;
1958
1971
  const desc = op.description ?? route.description;
1959
1972
  if (op.name || desc) {
1960
1973
  const tags = [];
1961
1974
  if (op.name) tags.push(`@name ${op.name}`);
1962
1975
  if (desc) tags.push(`@description ${desc}`);
1963
- if (tags.length === 1) {
1964
- lines.push(` /** ${tags[0]} */`);
1976
+ const contentLines = tags.flatMap((t) => escapeJsDocLines(t));
1977
+ if (contentLines.length === 1) {
1978
+ lines.push(` /** ${contentLines[0]} */`);
1965
1979
  } else {
1966
1980
  lines.push(` /**`);
1967
- for (const tag of tags) lines.push(` * ${tag}`);
1981
+ for (const l of contentLines) lines.push(` * ${l}`);
1968
1982
  lines.push(` */`);
1969
1983
  }
1970
1984
  }
@@ -2040,7 +2054,7 @@ function generateMethod(route, op, file, options) {
2040
2054
  }
2041
2055
  const readBodyExpr = respCategory === "text" ? `await result.text()` : respCategory === "binary" ? `await result.blob()` : `await parseJson<${dataType}>(result)`;
2042
2056
  if (hasRespHeaders) {
2043
- const headerEntries = respHeaders.map((h) => `${quoteKey2(headerNameToProperty(h.name))}: result.headers.get('${h.name}') ?? undefined`).join(", ");
2057
+ const headerEntries = respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}: result.headers.get('${h.name}') ?? undefined`).join(", ");
2044
2058
  if (isVoid) {
2045
2059
  lines.push(` return { headers: { ${headerEntries} } };`);
2046
2060
  } else {
@@ -2152,7 +2166,7 @@ function buildMethodParams(route, op, modelsWithInput) {
2152
2166
  }
2153
2167
  if (op.query) {
2154
2168
  if (op.query.kind === "params") {
2155
- const fields = op.query.nodes.map((p) => `${quoteKey2(p.name)}?: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
2169
+ const fields = op.query.nodes.map((p) => `${quoteKey(p.name)}?: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
2156
2170
  params.push({
2157
2171
  name: "query",
2158
2172
  type: `{ ${fields} }`,
@@ -2175,7 +2189,7 @@ function buildMethodParams(route, op, modelsWithInput) {
2175
2189
  }
2176
2190
  if (op.headers) {
2177
2191
  if (op.headers.kind === "params") {
2178
- const fields = op.headers.nodes.map((p) => `${quoteKey2(p.name)}?: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
2192
+ const fields = op.headers.nodes.map((p) => `${quoteKey(p.name)}?: ${renderInputTsType(p.type, modelsWithInput)}`).join("; ");
2179
2193
  params.push({
2180
2194
  name: "customHeaders",
2181
2195
  type: `{ ${fields} }`,
@@ -2877,7 +2891,7 @@ function generateComments2(model, outPath) {
2877
2891
  lines.push(` * @deprecated`);
2878
2892
  }
2879
2893
  if (model.description) {
2880
- lines.push(` * ${model.description}`);
2894
+ for (const l of escapeJsDocLines(model.description)) lines.push(` * ${l}`);
2881
2895
  }
2882
2896
  const relPath = outPath ? relative4(dirname4(outPath), model.loc.file) : model.loc.file;
2883
2897
  lines.push(` * generated from [${model.name}](file://./${relPath}#L${model.loc.line})`);
@@ -2941,34 +2955,40 @@ function generateVisibilityModel(model, outPath, modelsWithInput, modelMap) {
2941
2955
  return lines;
2942
2956
  }
2943
2957
  __name(generateVisibilityModel, "generateVisibilityModel");
2958
+ function withFieldJsDoc(jsdocParts, line) {
2959
+ if (jsdocParts.length === 0) return line;
2960
+ const contentLines = escapeJsDocLines(jsdocParts.join(" "));
2961
+ if (contentLines.length === 1) {
2962
+ return `/** ${contentLines[0]} */
2963
+ ${line}`;
2964
+ }
2965
+ const body = contentLines.map((l) => ` * ${l}`).join("\n");
2966
+ return `/**
2967
+ ${body}
2968
+ */
2969
+ ${line}`;
2970
+ }
2971
+ __name(withFieldJsDoc, "withFieldJsDoc");
2944
2972
  function renderField2(field) {
2945
2973
  const opt = field.optional || field.default !== void 0 ? "?" : "";
2946
2974
  let typeStr = renderTsType(field.type);
2947
2975
  if (field.nullable) typeStr += " | null";
2948
- const line = `${quoteKey2(field.name)}${opt}: ${typeStr};`;
2976
+ const line = `${quoteKey(field.name)}${opt}: ${typeStr};`;
2949
2977
  const jsdocParts = [];
2950
2978
  if (field.deprecated) jsdocParts.push("@deprecated");
2951
2979
  if (field.description) jsdocParts.push(field.description);
2952
- if (jsdocParts.length > 0) {
2953
- return `/** ${jsdocParts.join(" ")} */
2954
- ${line}`;
2955
- }
2956
- return line;
2980
+ return withFieldJsDoc(jsdocParts, line);
2957
2981
  }
2958
2982
  __name(renderField2, "renderField");
2959
2983
  function renderInputField2(field, modelsWithInput) {
2960
2984
  const opt = field.optional || field.default !== void 0 ? "?" : "";
2961
2985
  let typeStr = renderInputTsType(field.type, modelsWithInput);
2962
2986
  if (field.nullable) typeStr += " | null";
2963
- const line = `${quoteKey2(field.name)}${opt}: ${typeStr};`;
2987
+ const line = `${quoteKey(field.name)}${opt}: ${typeStr};`;
2964
2988
  const jsdocParts = [];
2965
2989
  if (field.deprecated) jsdocParts.push("@deprecated");
2966
2990
  if (field.description) jsdocParts.push(field.description);
2967
- if (jsdocParts.length > 0) {
2968
- return `/** ${jsdocParts.join(" ")} */
2969
- ${line}`;
2970
- }
2971
- return line;
2991
+ return withFieldJsDoc(jsdocParts, line);
2972
2992
  }
2973
2993
  __name(renderInputField2, "renderInputField");
2974
2994
  function camelToSnake2(s) {
@@ -3011,22 +3031,457 @@ function renderOutputField(field, outputCase, modelsWithOutput) {
3011
3031
  const key = applyOutputCase(field.name, outputCase);
3012
3032
  let typeStr = renderOutputTsType(field.type, modelsWithOutput);
3013
3033
  if (field.nullable) typeStr += " | null";
3014
- const line = `${quoteKey2(key)}${opt}: ${typeStr};`;
3034
+ const line = `${quoteKey(key)}${opt}: ${typeStr};`;
3015
3035
  const jsdocParts = [];
3016
3036
  if (field.deprecated) jsdocParts.push("@deprecated");
3017
3037
  if (field.description) jsdocParts.push(field.description);
3018
- if (jsdocParts.length > 0) {
3019
- return `/** ${jsdocParts.join(" ")} */
3020
- ${line}`;
3021
- }
3022
- return line;
3038
+ return withFieldJsDoc(jsdocParts, line);
3023
3039
  }
3024
3040
  __name(renderOutputField, "renderOutputField");
3025
3041
 
3042
+ // src/codegen-mcp.ts
3043
+ import { resolveModifiers as resolveModifiers3 } from "@contractkit/core";
3044
+ import { basename as basename3, dirname as dirname5, relative as relative5 } from "path";
3045
+ function mcpConfig(op) {
3046
+ return op.mcp && typeof op.mcp === "object" ? op.mcp : void 0;
3047
+ }
3048
+ __name(mcpConfig, "mcpConfig");
3049
+ function hasMcpOperations(root, includeInternal = false) {
3050
+ for (const route of root.routes) {
3051
+ for (const op of route.operations) {
3052
+ if (!op.mcp) continue;
3053
+ if (!includeInternal && resolveModifiers3(route, op).includes("internal")) continue;
3054
+ return true;
3055
+ }
3056
+ }
3057
+ return false;
3058
+ }
3059
+ __name(hasMcpOperations, "hasMcpOperations");
3060
+ function toSnake(s) {
3061
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[\s\-.]+/g, "_").toLowerCase().replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_|_$/g, "");
3062
+ }
3063
+ __name(toSnake, "toSnake");
3064
+ function toPascal(s) {
3065
+ return s.split("_").filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
3066
+ }
3067
+ __name(toPascal, "toPascal");
3068
+ function inferToolName(method, path) {
3069
+ const parts = [
3070
+ method.toLowerCase()
3071
+ ];
3072
+ for (const seg of path.split("/").filter(Boolean)) {
3073
+ if (seg.startsWith("{")) {
3074
+ parts.push("by", toSnake(seg.slice(1, -1)));
3075
+ } else {
3076
+ parts.push(toSnake(seg));
3077
+ }
3078
+ }
3079
+ return parts.filter(Boolean).join("_");
3080
+ }
3081
+ __name(inferToolName, "inferToolName");
3082
+ function deriveToolName(op, route) {
3083
+ const cfg = mcpConfig(op);
3084
+ if (cfg?.name) return cfg.name;
3085
+ if (op.sdk) return toSnake(op.sdk);
3086
+ if (op.name) return toSnake(op.name);
3087
+ return inferToolName(op.method, route.path);
3088
+ }
3089
+ __name(deriveToolName, "deriveToolName");
3090
+ function deriveToolClassName(toolName) {
3091
+ return `${toPascal(toolName)}McpTool`;
3092
+ }
3093
+ __name(deriveToolClassName, "deriveToolClassName");
3094
+ function buildArgsProps(route, op, modelsWithInput) {
3095
+ const props = [];
3096
+ if (route.params) {
3097
+ if (route.params.kind === "params") {
3098
+ for (const node of route.params.nodes) {
3099
+ props.push({
3100
+ key: node.name,
3101
+ expr: renderInputType(node.type, modelsWithInput),
3102
+ optional: false
3103
+ });
3104
+ }
3105
+ } else if (route.params.kind === "ref") {
3106
+ props.push({
3107
+ key: "params",
3108
+ expr: refSchema(route.params.name, modelsWithInput),
3109
+ optional: false
3110
+ });
3111
+ } else {
3112
+ props.push({
3113
+ key: "params",
3114
+ expr: renderInputType(route.params.node, modelsWithInput),
3115
+ optional: false
3116
+ });
3117
+ }
3118
+ }
3119
+ const bodies = op.request?.bodies ?? [];
3120
+ if (bodies.length === 1 && bodies[0].contentType === "multipart/form-data") {
3121
+ props.push({
3122
+ key: "multipartBody",
3123
+ expr: "z.unknown()",
3124
+ optional: false
3125
+ });
3126
+ } else if (bodies.length === 1) {
3127
+ props.push({
3128
+ key: "body",
3129
+ expr: renderInputType(bodies[0].bodyType, modelsWithInput),
3130
+ optional: false
3131
+ });
3132
+ } else if (bodies.length > 1) {
3133
+ props.push({
3134
+ key: "body",
3135
+ expr: "z.unknown()",
3136
+ optional: false
3137
+ });
3138
+ }
3139
+ if (op.query) props.push({
3140
+ key: "query",
3141
+ expr: paramSourceSchema(op.query, modelsWithInput),
3142
+ optional: true
3143
+ });
3144
+ if (op.headers) props.push({
3145
+ key: "headers",
3146
+ expr: paramSourceSchema(op.headers, modelsWithInput),
3147
+ optional: true
3148
+ });
3149
+ return props;
3150
+ }
3151
+ __name(buildArgsProps, "buildArgsProps");
3152
+ function refSchema(name, modelsWithInput) {
3153
+ return modelsWithInput?.has(name) ? `${name}Input` : name;
3154
+ }
3155
+ __name(refSchema, "refSchema");
3156
+ function paramSourceSchema(src, modelsWithInput) {
3157
+ if (src.kind === "ref") return refSchema(src.name, modelsWithInput);
3158
+ if (src.kind === "type") return renderInputType(src.node, modelsWithInput);
3159
+ const fields = src.nodes.map((n) => `${quoteKey(n.name)}: ${renderInputType(n.type, modelsWithInput)}`).join(", ");
3160
+ return `z.object({ ${fields} })`;
3161
+ }
3162
+ __name(paramSourceSchema, "paramSourceSchema");
3163
+ function argsSchemaExpr(props) {
3164
+ if (props.length === 0) return "z.object({})";
3165
+ const fields = props.map((p) => `${quoteKey(p.key)}: ${p.expr}${p.optional ? ".optional()" : ""}`).join(", ");
3166
+ return `z.object({ ${fields} })`;
3167
+ }
3168
+ __name(argsSchemaExpr, "argsSchemaExpr");
3169
+ function primaryResponse(op) {
3170
+ return op.responses.find((r) => r.bodyType) ?? op.responses[0];
3171
+ }
3172
+ __name(primaryResponse, "primaryResponse");
3173
+ function outputSchemaExpr(op) {
3174
+ const body = primaryResponse(op)?.bodyType;
3175
+ if (!body) return void 0;
3176
+ if (body.kind === "ref") return body.name;
3177
+ if (body.kind === "inlineObject") return renderType(body);
3178
+ return void 0;
3179
+ }
3180
+ __name(outputSchemaExpr, "outputSchemaExpr");
3181
+ var HINT_KEYS = [
3182
+ "readOnlyHint",
3183
+ "destructiveHint",
3184
+ "idempotentHint",
3185
+ "openWorldHint"
3186
+ ];
3187
+ function annotationsExpr(cfg) {
3188
+ if (!cfg) return void 0;
3189
+ const parts = [];
3190
+ for (const key of HINT_KEYS) {
3191
+ const val = cfg[key];
3192
+ if (val !== void 0) parts.push(`${key}: ${val}`);
3193
+ }
3194
+ return parts.length > 0 ? `{ ${parts.join(", ")} }` : void 0;
3195
+ }
3196
+ __name(annotationsExpr, "annotationsExpr");
3197
+ function walkTypeRefs(type, ids, variant, modelsWithInput) {
3198
+ switch (type.kind) {
3199
+ case "ref":
3200
+ ids.add(variant === "input" ? refSchema(type.name, modelsWithInput) : type.name);
3201
+ break;
3202
+ case "array":
3203
+ walkTypeRefs(type.item, ids, variant, modelsWithInput);
3204
+ break;
3205
+ case "tuple":
3206
+ type.items.forEach((t) => walkTypeRefs(t, ids, variant, modelsWithInput));
3207
+ break;
3208
+ case "record":
3209
+ walkTypeRefs(type.key, ids, variant, modelsWithInput);
3210
+ walkTypeRefs(type.value, ids, variant, modelsWithInput);
3211
+ break;
3212
+ case "union":
3213
+ case "discriminatedUnion":
3214
+ case "intersection":
3215
+ type.members.forEach((t) => walkTypeRefs(t, ids, variant, modelsWithInput));
3216
+ break;
3217
+ case "inlineObject":
3218
+ type.fields.forEach((f) => walkTypeRefs(f.type, ids, variant, modelsWithInput));
3219
+ break;
3220
+ case "lazy":
3221
+ walkTypeRefs(type.inner, ids, variant, modelsWithInput);
3222
+ break;
3223
+ }
3224
+ }
3225
+ __name(walkTypeRefs, "walkTypeRefs");
3226
+ function walkSourceRefs(src, ids, modelsWithInput) {
3227
+ if (!src) return;
3228
+ if (src.kind === "ref") ids.add(refSchema(src.name, modelsWithInput));
3229
+ else if (src.kind === "params") src.nodes.forEach((n) => walkTypeRefs(n.type, ids, "input", modelsWithInput));
3230
+ else walkTypeRefs(src.node, ids, "input", modelsWithInput);
3231
+ }
3232
+ __name(walkSourceRefs, "walkSourceRefs");
3233
+ function collectSchemaIds(ops, modelsWithInput) {
3234
+ const ids = /* @__PURE__ */ new Set();
3235
+ for (const { route, op } of ops) {
3236
+ walkSourceRefs(route.params, ids, modelsWithInput);
3237
+ const bodies = op.request?.bodies ?? [];
3238
+ if (bodies.length === 1 && bodies[0].contentType !== "multipart/form-data") {
3239
+ walkTypeRefs(bodies[0].bodyType, ids, "input", modelsWithInput);
3240
+ }
3241
+ walkSourceRefs(op.query, ids, modelsWithInput);
3242
+ walkSourceRefs(op.headers, ids, modelsWithInput);
3243
+ const body = primaryResponse(op)?.bodyType;
3244
+ if (body && (body.kind === "ref" || body.kind === "inlineObject")) walkTypeRefs(body, ids, "read");
3245
+ }
3246
+ return ids;
3247
+ }
3248
+ __name(collectSchemaIds, "collectSchemaIds");
3249
+ function schemaImportLines(ids, options) {
3250
+ const lines = [];
3251
+ const { modelOutPaths, outPath } = options;
3252
+ if (ids.size === 0) return lines;
3253
+ if (modelOutPaths && outPath) {
3254
+ const byFile = /* @__PURE__ */ new Map();
3255
+ const unresolved = [];
3256
+ for (const id of ids) {
3257
+ const p = modelOutPaths.get(id);
3258
+ if (p) {
3259
+ const group = byFile.get(p) ?? [];
3260
+ group.push(id);
3261
+ byFile.set(p, group);
3262
+ } else {
3263
+ unresolved.push(id);
3264
+ }
3265
+ }
3266
+ const fromDir = dirname5(outPath);
3267
+ for (const [file, names] of byFile) {
3268
+ let rel = relative5(fromDir, file).replace(/\.ts$/, ".js");
3269
+ if (!rel.startsWith(".")) rel = "./" + rel;
3270
+ lines.push(`import { ${names.sort().join(", ")} } from '${rel}';`);
3271
+ }
3272
+ for (const id of unresolved.sort()) lines.push(`import { ${id} } from './${pascalToDotCase(id)}.js';`);
3273
+ } else {
3274
+ for (const id of [
3275
+ ...ids
3276
+ ].sort()) lines.push(`import { ${id} } from './${pascalToDotCase(id)}.js';`);
3277
+ }
3278
+ return lines;
3279
+ }
3280
+ __name(schemaImportLines, "schemaImportLines");
3281
+ function scalarHelperLines(body) {
3282
+ const lines = [];
3283
+ if (body.includes("_ZodBinary")) {
3284
+ lines.push(`const _ZodBinary = z.custom<Buffer>((val) => Buffer.isBuffer(val), { error: 'Must be binary data' });`);
3285
+ }
3286
+ if (body.includes("_ZodDatetime")) {
3287
+ lines.push(`const _ZodDatetime = z.preprocess((val) => typeof val === 'string' ? DateTime.fromISO(val) : val, z.custom<DateTime>((val) => val instanceof DateTime && val.isValid, { message: 'Must be in ISO 8601 format' }));`);
3288
+ }
3289
+ if (body.includes("_ZodInterval")) {
3290
+ lines.push(`const _ZodInterval = z.preprocess((val) => typeof val === 'string' ? Interval.fromISO(val) : val, z.custom<Interval>((val) => val instanceof Interval && val.isValid, { message: 'Must be an ISO 8601 interval' })).transform(val => val.toISO()!);`);
3291
+ }
3292
+ if (body.includes("_ZodJson")) {
3293
+ lines.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
3294
+ lines.push(`const _ZodJson: z.ZodType<_JsonValue> = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(_ZodJson), z.record(z.string(), _ZodJson)]));`);
3295
+ }
3296
+ return lines;
3297
+ }
3298
+ __name(scalarHelperLines, "scalarHelperLines");
3299
+ function planTools(root, includeInternal) {
3300
+ const plans = [];
3301
+ for (const route of root.routes) {
3302
+ for (const op of route.operations) {
3303
+ if (!op.mcp) continue;
3304
+ if (!includeInternal && resolveModifiers3(route, op).includes("internal")) continue;
3305
+ const toolName = deriveToolName(op, route);
3306
+ const className = deriveToolClassName(toolName);
3307
+ plans.push({
3308
+ route,
3309
+ op,
3310
+ toolName,
3311
+ className,
3312
+ argsConstName: `${toPascal(toolName)}Args`
3313
+ });
3314
+ }
3315
+ }
3316
+ return plans;
3317
+ }
3318
+ __name(planTools, "planTools");
3319
+ function renderToolClass(plan, file, options) {
3320
+ const { route, op, toolName, className, argsConstName } = plan;
3321
+ const cfg = mcpConfig(op);
3322
+ const lines = [];
3323
+ const relFile = options.outPath ? relative5(dirname5(options.outPath), file) : file;
3324
+ lines.push("/**");
3325
+ lines.push(` * from [${basename3(file)}](file://./${relFile}#L${op.loc.line})`);
3326
+ lines.push(" */");
3327
+ lines.push("@Injectable()");
3328
+ lines.push(`export class ${className} implements McpToolHandler {`);
3329
+ lines.push(" readonly definition: Tool = {");
3330
+ lines.push(` name: '${escapeSingleQuoted(toolName)}',`);
3331
+ if (cfg?.title) lines.push(` title: '${escapeSingleQuoted(cfg.title)}',`);
3332
+ const desc = cfg?.description ?? op.description ?? route.description;
3333
+ if (desc) lines.push(` description: '${escapeSingleQuoted(desc)}',`);
3334
+ lines.push(` inputSchema: z.toJSONSchema(${argsConstName}, { unrepresentable: 'any' }) as Tool['inputSchema'],`);
3335
+ const outExpr = outputSchemaExpr(op);
3336
+ if (outExpr) lines.push(` outputSchema: z.toJSONSchema(${outExpr}, { unrepresentable: 'any' }) as Tool['outputSchema'],`);
3337
+ const annotations = annotationsExpr(cfg);
3338
+ if (annotations) lines.push(` annotations: ${annotations},`);
3339
+ lines.push(" };");
3340
+ lines.push("");
3341
+ const service = inferService(op, route, file);
3342
+ lines.push(` constructor(private readonly service: ${service.className}) {}`);
3343
+ lines.push("");
3344
+ const props = buildArgsProps(route, op, options.modelsWithInput);
3345
+ const destructure = props.map((p) => p.key);
3346
+ const callArgs = buildArgs(route, op);
3347
+ const isVoid = !primaryResponse(op)?.bodyType;
3348
+ const structured = !!outExpr;
3349
+ lines.push(" async handle(args: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {");
3350
+ if (destructure.length > 0) {
3351
+ lines.push(` const { ${destructure.join(", ")} } = await parseAndValidate(args, ${argsConstName});`);
3352
+ }
3353
+ if (isVoid) {
3354
+ lines.push(` await this.service.${service.methodName}(${callArgs});`);
3355
+ lines.push(` return { content: [{ type: 'text', text: 'OK' }] };`);
3356
+ } else {
3357
+ lines.push(` const result = await this.service.${service.methodName}(${callArgs});`);
3358
+ if (structured) {
3359
+ lines.push(` return { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result };`);
3360
+ } else {
3361
+ lines.push(` return { content: [{ type: 'text', text: JSON.stringify(result) }] };`);
3362
+ }
3363
+ }
3364
+ lines.push(" }");
3365
+ lines.push("}");
3366
+ return lines;
3367
+ }
3368
+ __name(renderToolClass, "renderToolClass");
3369
+ function deriveMcpRegisterFnName(file) {
3370
+ return `register${deriveBaseName(file)}McpTools`;
3371
+ }
3372
+ __name(deriveMcpRegisterFnName, "deriveMcpRegisterFnName");
3373
+ function generateMcpFile(root, options = {}) {
3374
+ const includeInternal = options.includeInternal ?? false;
3375
+ const plans = planTools(root, includeInternal);
3376
+ const argsConsts = plans.map((p) => `const ${p.argsConstName} = ${argsSchemaExpr(buildArgsProps(p.route, p.op, options.modelsWithInput))};`);
3377
+ const classes = plans.map((p) => renderToolClass(p, root.file, options).join("\n"));
3378
+ const registerFn = [];
3379
+ registerFn.push(`/** Add this file's tools to the shared catalog. */`);
3380
+ registerFn.push(`export function ${deriveMcpRegisterFnName(root.file)}(map: McpToolHandlerMap, container: Container): void {`);
3381
+ for (const p of plans) registerFn.push(` map.set('${escapeSingleQuoted(p.toolName)}', container.get(${p.className}));`);
3382
+ registerFn.push("}");
3383
+ const bodyCore = [
3384
+ argsConsts.join("\n"),
3385
+ classes.join("\n\n"),
3386
+ registerFn.join("\n")
3387
+ ].filter(Boolean).join("\n\n");
3388
+ const helperConsts = scalarHelperLines(bodyCore);
3389
+ const bodyWithHelpers = [
3390
+ helperConsts.join("\n"),
3391
+ bodyCore
3392
+ ].filter(Boolean).join("\n\n");
3393
+ const needsParseAndValidate = plans.some((p) => buildArgsProps(p.route, p.op, options.modelsWithInput).length > 0);
3394
+ const imports = [];
3395
+ imports.push(`import { Injectable, type Container } from 'injectkit';`);
3396
+ imports.push(`import { z } from 'zod';`);
3397
+ const luxon = [];
3398
+ if (/\bDateTime\b/.test(bodyWithHelpers)) luxon.push("DateTime");
3399
+ if (/\bInterval\b/.test(bodyWithHelpers)) luxon.push("Interval");
3400
+ if (/\bDuration\b/.test(bodyWithHelpers)) luxon.push("Duration");
3401
+ if (luxon.length > 0) imports.push(`import { ${luxon.join(", ")} } from 'luxon';`);
3402
+ imports.push(`import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';`);
3403
+ imports.push(`import type { McpToolHandler, McpToolHandlerMap, McpToolContext } from '@maroonedsoftware/mcp';`);
3404
+ if (needsParseAndValidate) imports.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
3405
+ const serviceModules = /* @__PURE__ */ new Map();
3406
+ for (const p of plans) {
3407
+ const svc = inferService(p.op, p.route, root.file).className;
3408
+ if (!serviceModules.has(svc)) {
3409
+ serviceModules.set(svc, root.services?.[svc] ?? root.meta[svc] ?? deriveModulePath(svc, options.servicePathTemplate));
3410
+ }
3411
+ }
3412
+ for (const [svc, mod] of [
3413
+ ...serviceModules.entries()
3414
+ ].sort(([a], [b]) => a.localeCompare(b))) {
3415
+ imports.push(`import { ${svc} } from '${mod}';`);
3416
+ }
3417
+ imports.push(...schemaImportLines(collectSchemaIds(plans, options.modelsWithInput), options));
3418
+ const relFile = options.outPath ? relative5(dirname5(options.outPath), root.file) : root.file;
3419
+ const header = `// Auto-generated MCP tools
3420
+ // generated from [${basename3(root.file)}](file://./${relFile})`;
3421
+ return `${header}
3422
+ ${imports.join("\n")}
3423
+
3424
+ ${bodyWithHelpers}
3425
+ `;
3426
+ }
3427
+ __name(generateMcpFile, "generateMcpFile");
3428
+ function generateMcpAggregator(entries) {
3429
+ const sorted = [
3430
+ ...entries
3431
+ ].sort((a, b) => a.registerFn.localeCompare(b.registerFn));
3432
+ const lines = [];
3433
+ lines.push(`import { type Container } from 'injectkit';`);
3434
+ lines.push(`import { McpToolHandlerMap } from '@maroonedsoftware/mcp';`);
3435
+ for (const e of sorted) lines.push(`import { ${e.registerFn} } from '${e.importPath}';`);
3436
+ lines.push("");
3437
+ lines.push("/** Build + register the MCP tool catalog. Call once at startup. */");
3438
+ lines.push("export function registerMcpTools(container: Container): McpToolHandlerMap {");
3439
+ lines.push(" const map = new McpToolHandlerMap();");
3440
+ for (const e of sorted) lines.push(` ${e.registerFn}(map, container);`);
3441
+ lines.push(" container.register(McpToolHandlerMap, { useValue: map });");
3442
+ lines.push(" return map;");
3443
+ lines.push("}");
3444
+ return lines.join("\n") + "\n";
3445
+ }
3446
+ __name(generateMcpAggregator, "generateMcpAggregator");
3447
+ function generateMcpRouter(options = {}) {
3448
+ const path = options.path ?? "/mcp";
3449
+ return `import { ServerKitRouter, requireSignature } from '@maroonedsoftware/koa';
3450
+ import { McpDispatcher, createMcpRequestContext, MCP_AUTH_POLICY } from '@maroonedsoftware/mcp';
3451
+
3452
+ /** Mount the MCP endpoint onto a ServerKit router. Call \`registerMcpTools(container)\` at startup. */
3453
+ export function mountMcp(router: ReturnType<typeof ServerKitRouter>): void {
3454
+ router.post('${path}', requireSignature('mcp', { policy: MCP_AUTH_POLICY }), async (ctx) => {
3455
+ const dispatcher = ctx.container.get(McpDispatcher);
3456
+ const context = createMcpRequestContext({ requestId: ctx.requestId, logger: ctx.logger });
3457
+ if (dispatcher.sessionMode === 'stateful') {
3458
+ ctx.respond = false;
3459
+ await dispatcher.dispatchStateful(
3460
+ { req: ctx.req, res: ctx.res, body: ctx.request.body, sessionId: ctx.get('mcp-session-id') },
3461
+ context,
3462
+ );
3463
+ } else {
3464
+ const response = await dispatcher.dispatch(JSON.parse(ctx.rawBody), context);
3465
+ if (response) ctx.body = response;
3466
+ }
3467
+ });
3468
+ }
3469
+ `;
3470
+ }
3471
+ __name(generateMcpRouter, "generateMcpRouter");
3472
+
3026
3473
  // src/path-utils.ts
3027
- import { resolve, join, relative as relative5, dirname as dirname5 } from "path";
3474
+ import { resolve, join, relative as relative6, dirname as dirname6, isAbsolute } from "path";
3028
3475
  import { collectTypeRefs as collectTypeRefs2, collectPublicTypeNames } from "@contractkit/core";
3029
3476
  var TEMPLATE_VAR_RE = /\{\w+\}/;
3477
+ function assertWithinBase(baseOutDir, outPath) {
3478
+ const rel = relative6(resolve(baseOutDir), resolve(outPath));
3479
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
3480
+ throw new Error(`Refusing to emit outside output directory: resolved path "${outPath}" escapes "${baseOutDir}" (check options { keys } values used in output path templates)`);
3481
+ }
3482
+ return outPath;
3483
+ }
3484
+ __name(assertWithinBase, "assertWithinBase");
3030
3485
  function resolveTemplate(template, vars) {
3031
3486
  return template.replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? `{${key}}`);
3032
3487
  }
@@ -3038,7 +3493,7 @@ function includesFilename(p) {
3038
3493
  __name(includesFilename, "includesFilename");
3039
3494
  function commonDir(files, rootDir) {
3040
3495
  if (files.length === 0) return resolve(rootDir);
3041
- const parts = files.map((f) => dirname5(f).split("/"));
3496
+ const parts = files.map((f) => dirname6(f).split("/"));
3042
3497
  const first = parts[0];
3043
3498
  let depth = first.length;
3044
3499
  for (const p of parts) {
@@ -3054,7 +3509,7 @@ function commonDir(files, rootDir) {
3054
3509
  __name(commonDir, "commonDir");
3055
3510
  function computeOpOutPath(filePath, baseDir, output, defaultSuffix, commonRoot, meta = {}) {
3056
3511
  const baseName = filePath.split("/").pop();
3057
- const relDir = relative5(commonRoot, dirname5(filePath));
3512
+ const relDir = relative6(commonRoot, dirname6(filePath));
3058
3513
  const filename = baseName.replace(/\.ck$/, "");
3059
3514
  const defaultName = `${filename}${defaultSuffix}`;
3060
3515
  const baseOutDir = resolve(baseDir);
@@ -3065,14 +3520,14 @@ function computeOpOutPath(filePath, baseDir, output, defaultSuffix, commonRoot,
3065
3520
  ext: "ck",
3066
3521
  ...meta
3067
3522
  });
3068
- if (includesFilename(resolved)) return join(baseOutDir, resolved);
3069
- return join(baseOutDir, resolved, defaultName);
3523
+ if (includesFilename(resolved)) return assertWithinBase(baseOutDir, join(baseOutDir, resolved));
3524
+ return assertWithinBase(baseOutDir, join(baseOutDir, resolved, defaultName));
3070
3525
  }
3071
3526
  if (output) {
3072
- if (includesFilename(output)) return join(baseOutDir, output);
3073
- return join(baseOutDir, output, relDir, defaultName);
3527
+ if (includesFilename(output)) return assertWithinBase(baseOutDir, join(baseOutDir, output));
3528
+ return assertWithinBase(baseOutDir, join(baseOutDir, output, relDir, defaultName));
3074
3529
  }
3075
- return join(baseOutDir, relDir, defaultName);
3530
+ return assertWithinBase(baseOutDir, join(baseOutDir, relDir, defaultName));
3076
3531
  }
3077
3532
  __name(computeOpOutPath, "computeOpOutPath");
3078
3533
  function computeContractOutPath(filePath, baseDir, output, defaultSuffix, commonRoot, meta = {}) {
@@ -3084,7 +3539,7 @@ function computeSdkOutPath(filePath, rootDir, clientOutput, commonRoot, meta = {
3084
3539
  const baseName = filePath.split("/").pop();
3085
3540
  const defaultOutName = baseName.replace(/\.ck$/, ".client.ts");
3086
3541
  const baseOutDir = resolve(rootDir);
3087
- const relDir = relative5(commonRoot, dirname5(filePath));
3542
+ const relDir = relative6(commonRoot, dirname6(filePath));
3088
3543
  const filename = baseName.replace(/\.ck$/, "");
3089
3544
  if (clientOutput && TEMPLATE_VAR_RE.test(clientOutput)) {
3090
3545
  const resolved = resolveTemplate(clientOutput, {
@@ -3093,14 +3548,14 @@ function computeSdkOutPath(filePath, rootDir, clientOutput, commonRoot, meta = {
3093
3548
  ext: "ck",
3094
3549
  ...meta
3095
3550
  });
3096
- if (includesFilename(resolved)) return join(baseOutDir, resolved);
3097
- return join(baseOutDir, resolved, defaultOutName);
3551
+ if (includesFilename(resolved)) return assertWithinBase(baseOutDir, join(baseOutDir, resolved));
3552
+ return assertWithinBase(baseOutDir, join(baseOutDir, resolved, defaultOutName));
3098
3553
  }
3099
3554
  if (clientOutput) {
3100
- if (includesFilename(clientOutput)) return join(baseOutDir, clientOutput);
3101
- return join(baseOutDir, clientOutput, relDir, defaultOutName);
3555
+ if (includesFilename(clientOutput)) return assertWithinBase(baseOutDir, join(baseOutDir, clientOutput));
3556
+ return assertWithinBase(baseOutDir, join(baseOutDir, clientOutput, relDir, defaultOutName));
3102
3557
  }
3103
- return join(baseOutDir, relDir, defaultOutName);
3558
+ return assertWithinBase(baseOutDir, join(baseOutDir, relDir, defaultOutName));
3104
3559
  }
3105
3560
  __name(computeSdkOutPath, "computeSdkOutPath");
3106
3561
  function computeSdkAreaClientOutPath(area, rootDir, clientOutput) {
@@ -3121,14 +3576,14 @@ function computeSdkAreaClientOutPath(area, rootDir, clientOutput) {
3121
3576
  subarea: ""
3122
3577
  });
3123
3578
  const cleaned = fixHiddenSegment(resolved.replace(/\/+/g, "/").replace(/^\//, ""));
3124
- if (includesFilename(cleaned)) return join(baseOutDir, cleaned);
3125
- return join(baseOutDir, cleaned, `${filename}.client.ts`);
3579
+ if (includesFilename(cleaned)) return assertWithinBase(baseOutDir, join(baseOutDir, cleaned));
3580
+ return assertWithinBase(baseOutDir, join(baseOutDir, cleaned, `${filename}.client.ts`));
3126
3581
  }
3127
3582
  if (clientOutput) {
3128
- if (includesFilename(clientOutput)) return join(baseOutDir, clientOutput);
3129
- return join(baseOutDir, clientOutput, `${filename}.client.ts`);
3583
+ if (includesFilename(clientOutput)) return assertWithinBase(baseOutDir, join(baseOutDir, clientOutput));
3584
+ return assertWithinBase(baseOutDir, join(baseOutDir, clientOutput, `${filename}.client.ts`));
3130
3585
  }
3131
- return join(baseOutDir, `${filename}.client.ts`);
3586
+ return assertWithinBase(baseOutDir, join(baseOutDir, `${filename}.client.ts`));
3132
3587
  }
3133
3588
  __name(computeSdkAreaClientOutPath, "computeSdkAreaClientOutPath");
3134
3589
  function computeSdkTypeOutPath(filePath, rootDir, typeOutput, commonRoot, meta = {}) {
@@ -3136,7 +3591,7 @@ function computeSdkTypeOutPath(filePath, rootDir, typeOutput, commonRoot, meta =
3136
3591
  const baseName = filePath.split("/").pop();
3137
3592
  const defaultOutName = baseName.replace(/\.ck$/, ".ts");
3138
3593
  const baseOutDir = resolve(rootDir);
3139
- const relDir = relative5(commonRoot, dirname5(filePath));
3594
+ const relDir = relative6(commonRoot, dirname6(filePath));
3140
3595
  const filename = baseName.replace(/\.ck$/, "");
3141
3596
  if (TEMPLATE_VAR_RE.test(typeOutput)) {
3142
3597
  const resolved = resolveTemplate(typeOutput, {
@@ -3145,17 +3600,17 @@ function computeSdkTypeOutPath(filePath, rootDir, typeOutput, commonRoot, meta =
3145
3600
  ext: "ck",
3146
3601
  ...meta
3147
3602
  });
3148
- if (includesFilename(resolved)) return join(baseOutDir, resolved);
3149
- return join(baseOutDir, resolved, defaultOutName);
3603
+ if (includesFilename(resolved)) return assertWithinBase(baseOutDir, join(baseOutDir, resolved));
3604
+ return assertWithinBase(baseOutDir, join(baseOutDir, resolved, defaultOutName));
3150
3605
  }
3151
- if (includesFilename(typeOutput)) return join(baseOutDir, typeOutput);
3152
- return join(baseOutDir, typeOutput, relDir, defaultOutName);
3606
+ if (includesFilename(typeOutput)) return assertWithinBase(baseOutDir, join(baseOutDir, typeOutput));
3607
+ return assertWithinBase(baseOutDir, join(baseOutDir, typeOutput, relDir, defaultOutName));
3153
3608
  }
3154
3609
  __name(computeSdkTypeOutPath, "computeSdkTypeOutPath");
3155
3610
  function generateBarrelFiles(contractPaths) {
3156
3611
  const byDir = /* @__PURE__ */ new Map();
3157
3612
  for (const outPath of contractPaths) {
3158
- const dir = dirname5(outPath);
3613
+ const dir = dirname6(outPath);
3159
3614
  const group = byDir.get(dir) ?? [];
3160
3615
  group.push(outPath);
3161
3616
  byDir.set(dir, group);
@@ -3249,6 +3704,7 @@ async function runTypescriptCodegen(inputs, ctx, config, rootDir) {
3249
3704
  if (config.sdk) collectSdkOutput(config.sdk, rootDir, inputs, units, globalFiles);
3250
3705
  if (config.zod) collectZodOutput(config.zod, rootDir, inputs, units);
3251
3706
  if (config.types) collectTypesOutput(config.types, rootDir, inputs, units);
3707
+ if (config.mcp) collectMcpOutput(config.mcp, config, rootDir, inputs, units, globalFiles);
3252
3708
  const result = runIncrementalCodegen({
3253
3709
  codegenVersion: TYPESCRIPT_CODEGEN_VERSION,
3254
3710
  prevManifest,
@@ -3455,7 +3911,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3455
3911
  const sdkEntryPath = sdkOutput ? join2(sdkBase, TEMPLATE_VAR_RE.test(sdkOutput) ? resolveTemplate(sdkOutput, {
3456
3912
  name: sdkName ?? "sdk"
3457
3913
  }) : sdkOutput) : join2(sdkBase, "sdk.ts");
3458
- const sdkOptionsPath = join2(dirname6(sdkEntryPath), "sdk-options.ts");
3914
+ const sdkOptionsPath = join2(dirname7(sdkEntryPath), "sdk-options.ts");
3459
3915
  const subConfigKey = stableSubConfig(config);
3460
3916
  const modelsWithInput = inputs.modelsWithInput;
3461
3917
  const modelsWithOutput = inputs.modelsWithOutput;
@@ -3514,7 +3970,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3514
3970
  modelsWithOutput
3515
3971
  });
3516
3972
  } else {
3517
- let rel = relative6(dirname6(typeOutPath), sdkOptionsPath).replace(/\.ts$/, ".js");
3973
+ let rel = relative7(dirname7(typeOutPath), sdkOptionsPath).replace(/\.ts$/, ".js");
3518
3974
  if (!rel.startsWith(".")) rel = "./" + rel;
3519
3975
  content = generatePlainTypes(ast, {
3520
3976
  modelOutPaths: sdkModelOutPaths,
@@ -3655,12 +4111,12 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3655
4111
  const hasAnything = sdkClientInfos.length > 0 || areaBuckets.size > 0;
3656
4112
  const areaClientOutPaths = /* @__PURE__ */ new Map();
3657
4113
  if (hasAnything) {
3658
- const sdkEntryDir = dirname6(sdkEntryPath);
3659
- const sdkOptionsRel = relative6(sdkEntryDir, sdkOptionsPath).replace(/\.ts$/, ".js");
4114
+ const sdkEntryDir = dirname7(sdkEntryPath);
4115
+ const sdkOptionsRel = relative7(sdkEntryDir, sdkOptionsPath).replace(/\.ts$/, ".js");
3660
4116
  const sdkOptionsImportPath = sdkOptionsRel.startsWith(".") ? sdkOptionsRel : "./" + sdkOptionsRel;
3661
4117
  const sdkClassName = sdkName ? sdkName.split(/[-._\s]+/).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Sdk" : "Sdk";
3662
4118
  const toClientImport = /* @__PURE__ */ __name((sourceDir, info) => {
3663
- let rel = relative6(sourceDir, info.outPath).replace(/\.ts$/, ".js");
4119
+ let rel = relative7(sourceDir, info.outPath).replace(/\.ts$/, ".js");
3664
4120
  if (!rel.startsWith(".")) rel = "./" + rel;
3665
4121
  return {
3666
4122
  className: info.className,
@@ -3689,7 +4145,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3689
4145
  });
3690
4146
  const subareaClients = bucket.leaves.sort((a, b) => a.subarea.localeCompare(b.subarea)).map((l) => ({
3691
4147
  propertyName: deriveSubareaPropertyName(l.subarea),
3692
- client: toClientImport(dirname6(areaClientOutPath), {
4148
+ client: toClientImport(dirname7(areaClientOutPath), {
3693
4149
  outPath: l.outPath,
3694
4150
  className: deriveSubareaClientClassName(area, l.subarea),
3695
4151
  propertyName: deriveSubareaPropertyName(l.subarea)
@@ -3760,23 +4216,23 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3760
4216
  })
3761
4217
  });
3762
4218
  }
3763
- const sdkSrcDir = dirname6(sdkEntryPath);
4219
+ const sdkSrcDir = dirname7(sdkEntryPath);
3764
4220
  const sdkTypeBarrels = generateBarrelFiles(sdkTypePaths);
3765
4221
  for (const barrel of sdkTypeBarrels) globalFiles.push({
3766
4222
  relativePath: barrel.outPath,
3767
4223
  content: barrel.content
3768
4224
  });
3769
4225
  const rootExports = [
3770
- `export * from './${basename3(sdkOptionsPath).replace(/\.ts$/, ".js")}';`
4226
+ `export * from './${basename4(sdkOptionsPath).replace(/\.ts$/, ".js")}';`
3771
4227
  ];
3772
- if (hasAnything) rootExports.push(`export * from './${basename3(sdkEntryPath).replace(/\.ts$/, ".js")}';`);
4228
+ if (hasAnything) rootExports.push(`export * from './${basename4(sdkEntryPath).replace(/\.ts$/, ".js")}';`);
3773
4229
  for (const c of sdkClientInfos) {
3774
- let rel = relative6(sdkSrcDir, c.outPath).replace(/\.ts$/, ".js");
4230
+ let rel = relative7(sdkSrcDir, c.outPath).replace(/\.ts$/, ".js");
3775
4231
  if (!rel.startsWith(".")) rel = "./" + rel;
3776
4232
  rootExports.push(`export * from '${rel}';`);
3777
4233
  }
3778
4234
  for (const barrel of sdkTypeBarrels) {
3779
- let rel = relative6(sdkSrcDir, barrel.outPath).replace(/\.ts$/, ".js");
4235
+ let rel = relative7(sdkSrcDir, barrel.outPath).replace(/\.ts$/, ".js");
3780
4236
  if (!rel.startsWith(".")) rel = "./" + rel;
3781
4237
  rootExports.push(`export * from '${rel}';`);
3782
4238
  }
@@ -3920,6 +4376,114 @@ function collectTypesOutput(config, rootDir, inputs, units) {
3920
4376
  }
3921
4377
  }
3922
4378
  __name(collectTypesOutput, "collectTypesOutput");
4379
+ function resolveMcpModelOutPaths(config, rootDir, contractRoots, commonRoot, modelsWithInput, modelsWithOutput) {
4380
+ const map = /* @__PURE__ */ new Map();
4381
+ let base;
4382
+ let template;
4383
+ let suffix;
4384
+ if (config.mcp?.output?.types) {
4385
+ base = resolve2(rootDir, config.mcp.baseDir ?? ".");
4386
+ template = config.mcp.output.types;
4387
+ suffix = ".ts";
4388
+ } else if (config.server?.zod && config.server.output?.types) {
4389
+ base = resolve2(rootDir, config.server.baseDir ?? ".");
4390
+ template = config.server.output.types;
4391
+ suffix = ".ts";
4392
+ } else if (config.zod) {
4393
+ base = resolve2(rootDir, config.zod.baseDir ?? ".");
4394
+ template = config.zod.output;
4395
+ suffix = ".schema.ts";
4396
+ } else {
4397
+ return map;
4398
+ }
4399
+ for (const ast of contractRoots) {
4400
+ const outPath = computeContractOutPath(ast.file, base, template, suffix, commonRoot, ast.meta);
4401
+ for (const model of ast.models) {
4402
+ map.set(model.name, outPath);
4403
+ if (modelsWithInput.has(model.name)) map.set(`${model.name}Input`, outPath);
4404
+ if (modelsWithOutput.has(model.name)) map.set(`${model.name}Output`, outPath);
4405
+ }
4406
+ }
4407
+ return map;
4408
+ }
4409
+ __name(resolveMcpModelOutPaths, "resolveMcpModelOutPaths");
4410
+ function collectMcpOutput(config, fullConfig, rootDir, inputs, units, globalFiles) {
4411
+ const mcpBase = resolve2(rootDir, config.baseDir ?? ".");
4412
+ const modelsWithInput = inputs.modelsWithInput;
4413
+ const modelsWithOutput = inputs.modelsWithOutput;
4414
+ const modelMap = buildModelMap(inputs.contractRoots);
4415
+ const allFiles = [
4416
+ ...inputs.contractRoots.map((r) => r.file),
4417
+ ...inputs.opRoots.map((r) => r.file)
4418
+ ];
4419
+ const commonRoot = commonDir(allFiles, rootDir);
4420
+ const subConfigKey = stableSubConfig(config);
4421
+ const includeInternal = config.includeInternal ?? false;
4422
+ const modelOutPaths = resolveMcpModelOutPaths(fullConfig, rootDir, inputs.contractRoots, commonRoot, modelsWithInput, modelsWithOutput);
4423
+ const entries = [];
4424
+ for (const ast of inputs.opRoots) {
4425
+ if (!hasMcpOperations(ast, includeInternal)) continue;
4426
+ const outPath = computeOpOutPath(ast.file, mcpBase, config.output?.tools, ".mcp.ts", commonRoot, ast.meta);
4427
+ const refs = collectOpRootRefs(ast, modelMap);
4428
+ const fingerprint = hashFingerprint({
4429
+ kind: "mcp-tools",
4430
+ v: TYPESCRIPT_CODEGEN_VERSION,
4431
+ outPath,
4432
+ root: ast,
4433
+ outPathSlice: sliceOutPathMap(refs, modelOutPaths, modelsWithInput, modelsWithOutput),
4434
+ modelsWithInput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithInput),
4435
+ modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
4436
+ servicePathTemplate: config.servicePathTemplate ?? null,
4437
+ includeInternal,
4438
+ sub: subConfigKey
4439
+ });
4440
+ units.push({
4441
+ key: `mcp-tools::${outPath}`,
4442
+ fingerprint,
4443
+ render: /* @__PURE__ */ __name(() => [
4444
+ {
4445
+ relativePath: outPath,
4446
+ content: generateMcpFile(ast, {
4447
+ outPath,
4448
+ modelOutPaths,
4449
+ modelsWithInput,
4450
+ modelsWithOutput,
4451
+ servicePathTemplate: config.servicePathTemplate,
4452
+ includeInternal
4453
+ })
4454
+ }
4455
+ ], "render")
4456
+ });
4457
+ entries.push({
4458
+ outPath,
4459
+ registerFn: deriveMcpRegisterFnName(ast.file)
4460
+ });
4461
+ }
4462
+ if (entries.length === 0) return;
4463
+ const indexPath = join2(mcpBase, config.output?.index ?? "mcp.tools.ts");
4464
+ const aggregatorEntries = entries.map((e) => {
4465
+ let rel = relative7(dirname7(indexPath), e.outPath).replace(/\.ts$/, ".js");
4466
+ if (!rel.startsWith(".")) rel = "./" + rel;
4467
+ return {
4468
+ registerFn: e.registerFn,
4469
+ importPath: rel
4470
+ };
4471
+ }).sort((a, b) => a.registerFn.localeCompare(b.registerFn));
4472
+ globalFiles.push({
4473
+ relativePath: indexPath,
4474
+ content: generateMcpAggregator(aggregatorEntries)
4475
+ });
4476
+ if (config.emitRouter !== false) {
4477
+ const routerPath = join2(mcpBase, config.output?.router ?? "mcp.router.ts");
4478
+ globalFiles.push({
4479
+ relativePath: routerPath,
4480
+ content: generateMcpRouter({
4481
+ path: config.path
4482
+ })
4483
+ });
4484
+ }
4485
+ }
4486
+ __name(collectMcpOutput, "collectMcpOutput");
3923
4487
  function readManifest(manifestPath) {
3924
4488
  if (!existsSync(manifestPath)) return emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION);
3925
4489
  try {
@@ -3931,7 +4495,7 @@ function readManifest(manifestPath) {
3931
4495
  __name(readManifest, "readManifest");
3932
4496
  function writeManifest(manifestPath, manifest) {
3933
4497
  try {
3934
- mkdirSync(dirname6(manifestPath), {
4498
+ mkdirSync(dirname7(manifestPath), {
3935
4499
  recursive: true
3936
4500
  });
3937
4501
  writeFileSync(manifestPath, serializeIncrementalManifest(manifest), "utf-8");
@@ -3947,7 +4511,7 @@ function deleteStalePaths(absPaths) {
3947
4511
  rmSync(abs, {
3948
4512
  force: true
3949
4513
  });
3950
- removedDirs.add(dirname6(abs));
4514
+ removedDirs.add(dirname7(abs));
3951
4515
  }
3952
4516
  }
3953
4517
  for (const dir of removedDirs) {
@@ -3956,7 +4520,7 @@ function deleteStalePaths(absPaths) {
3956
4520
  try {
3957
4521
  if (readdirSync(current).length === 0) {
3958
4522
  rmdirSync(current);
3959
- current = dirname6(current);
4523
+ current = dirname7(current);
3960
4524
  } else {
3961
4525
  break;
3962
4526
  }