@contractkit/plugin-typescript 0.28.1 → 0.29.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/dist/index.js CHANGED
@@ -2,7 +2,7 @@ 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
@@ -31,41 +31,41 @@ function headerNameToProperty(name) {
31
31
  }).join("");
32
32
  }
33
33
  __name(headerNameToProperty, "headerNameToProperty");
34
- function renderTsType(type) {
34
+ function renderTsType(type, target = "client") {
35
35
  switch (type.kind) {
36
36
  case "scalar":
37
- return renderTsScalar(type.name);
37
+ return renderTsScalar(type.name, target);
38
38
  case "array": {
39
- const inner = renderTsType(type.item);
39
+ const inner = renderTsType(type.item, target);
40
40
  const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
41
41
  return needsParens ? `(${inner})[]` : `${inner}[]`;
42
42
  }
43
43
  case "tuple":
44
- return `[${type.items.map(renderTsType).join(", ")}]`;
44
+ return `[${type.items.map((i) => renderTsType(i, target)).join(", ")}]`;
45
45
  case "record":
46
- return `Record<${renderTsType(type.key)}, ${renderTsType(type.value)}>`;
46
+ return `Record<${renderTsType(type.key, target)}, ${renderTsType(type.value, target)}>`;
47
47
  case "enum":
48
48
  return type.values.map((v) => `'${escapeSingleQuoted(v)}'`).join(" | ");
49
49
  case "literal":
50
50
  return typeof type.value === "string" ? `'${escapeSingleQuoted(type.value)}'` : String(type.value);
51
51
  case "union":
52
- return type.members.map(renderTsType).join(" | ");
52
+ return type.members.map((m) => renderTsType(m, target)).join(" | ");
53
53
  case "discriminatedUnion":
54
- return type.members.map(renderTsType).join(" | ");
54
+ return type.members.map((m) => renderTsType(m, target)).join(" | ");
55
55
  case "intersection":
56
- return type.members.map(renderTsType).join(" & ");
56
+ return type.members.map((m) => renderTsType(m, target)).join(" & ");
57
57
  case "ref":
58
58
  return type.name;
59
59
  case "lazy":
60
- return renderTsType(type.inner);
60
+ return renderTsType(type.inner, target);
61
61
  case "inlineObject":
62
- return renderTsInlineObject(type.fields);
62
+ return renderTsInlineObject(type.fields, target);
63
63
  default:
64
64
  return "unknown";
65
65
  }
66
66
  }
67
67
  __name(renderTsType, "renderTsType");
68
- function renderTsScalar(name) {
68
+ function renderTsScalar(name, target) {
69
69
  switch (name) {
70
70
  case "string":
71
71
  case "email":
@@ -92,7 +92,7 @@ function renderTsScalar(name) {
92
92
  case "object":
93
93
  return "Record<string, unknown>";
94
94
  case "binary":
95
- return "Blob";
95
+ return target === "server" ? "Buffer" : "Blob";
96
96
  case "json":
97
97
  return "JsonValue";
98
98
  default: {
@@ -102,61 +102,61 @@ function renderTsScalar(name) {
102
102
  }
103
103
  }
104
104
  __name(renderTsScalar, "renderTsScalar");
105
- function renderTsInlineObject(fields) {
105
+ function renderTsInlineObject(fields, target) {
106
106
  const entries = fields.map((f) => {
107
107
  const opt = f.optional ? "?" : "";
108
- return `${quoteKey(f.name)}${opt}: ${renderTsType(f.type)}`;
108
+ return `${quoteKey(f.name)}${opt}: ${renderTsType(f.type, target)}`;
109
109
  });
110
110
  return `{ ${entries.join("; ")} }`;
111
111
  }
112
112
  __name(renderTsInlineObject, "renderTsInlineObject");
113
- function renderInputTsType(type, modelsWithInput) {
114
- if (!modelsWithInput || modelsWithInput.size === 0) return renderTsType(type);
113
+ function renderInputTsType(type, modelsWithInput, target = "client") {
114
+ if (!modelsWithInput || modelsWithInput.size === 0) return renderTsType(type, target);
115
115
  switch (type.kind) {
116
116
  case "ref":
117
117
  return modelsWithInput.has(type.name) ? `${type.name}Input` : type.name;
118
118
  case "array": {
119
- const inner = renderInputTsType(type.item, modelsWithInput);
119
+ const inner = renderInputTsType(type.item, modelsWithInput, target);
120
120
  const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
121
121
  return needsParens ? `(${inner})[]` : `${inner}[]`;
122
122
  }
123
123
  case "intersection":
124
- return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" & ");
124
+ return type.members.map((m) => renderInputTsType(m, modelsWithInput, target)).join(" & ");
125
125
  case "union":
126
- return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" | ");
126
+ return type.members.map((m) => renderInputTsType(m, modelsWithInput, target)).join(" | ");
127
127
  case "discriminatedUnion":
128
- return type.members.map((m) => renderInputTsType(m, modelsWithInput)).join(" | ");
128
+ return type.members.map((m) => renderInputTsType(m, modelsWithInput, target)).join(" | ");
129
129
  case "inlineObject":
130
- return `{ ${type.fields.map((f) => `${quoteKey(f.name)}${f.optional ? "?" : ""}: ${renderInputTsType(f.type, modelsWithInput)}`).join("; ")} }`;
130
+ return `{ ${type.fields.map((f) => `${quoteKey(f.name)}${f.optional ? "?" : ""}: ${renderInputTsType(f.type, modelsWithInput, target)}`).join("; ")} }`;
131
131
  case "lazy":
132
- return renderInputTsType(type.inner, modelsWithInput);
132
+ return renderInputTsType(type.inner, modelsWithInput, target);
133
133
  default:
134
- return renderTsType(type);
134
+ return renderTsType(type, target);
135
135
  }
136
136
  }
137
137
  __name(renderInputTsType, "renderInputTsType");
138
- function renderOutputTsType(type, modelsWithOutput) {
139
- if (!modelsWithOutput || modelsWithOutput.size === 0) return renderTsType(type);
138
+ function renderOutputTsType(type, modelsWithOutput, target = "client") {
139
+ if (!modelsWithOutput || modelsWithOutput.size === 0) return renderTsType(type, target);
140
140
  switch (type.kind) {
141
141
  case "ref":
142
142
  return modelsWithOutput.has(type.name) ? `${type.name}Output` : type.name;
143
143
  case "array": {
144
- const inner = renderOutputTsType(type.item, modelsWithOutput);
144
+ const inner = renderOutputTsType(type.item, modelsWithOutput, target);
145
145
  const needsParens = type.item.kind === "union" || type.item.kind === "discriminatedUnion" || type.item.kind === "intersection" || type.item.kind === "enum";
146
146
  return needsParens ? `(${inner})[]` : `${inner}[]`;
147
147
  }
148
148
  case "intersection":
149
- return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" & ");
149
+ return type.members.map((m) => renderOutputTsType(m, modelsWithOutput, target)).join(" & ");
150
150
  case "union":
151
- return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" | ");
151
+ return type.members.map((m) => renderOutputTsType(m, modelsWithOutput, target)).join(" | ");
152
152
  case "discriminatedUnion":
153
- return type.members.map((m) => renderOutputTsType(m, modelsWithOutput)).join(" | ");
153
+ return type.members.map((m) => renderOutputTsType(m, modelsWithOutput, target)).join(" | ");
154
154
  case "inlineObject":
155
- return `{ ${type.fields.map((f) => `${quoteKey(f.name)}${f.optional ? "?" : ""}: ${renderOutputTsType(f.type, modelsWithOutput)}`).join("; ")} }`;
155
+ return `{ ${type.fields.map((f) => `${quoteKey(f.name)}${f.optional ? "?" : ""}: ${renderOutputTsType(f.type, modelsWithOutput, target)}`).join("; ")} }`;
156
156
  case "lazy":
157
- return renderOutputTsType(type.inner, modelsWithOutput);
157
+ return renderOutputTsType(type.inner, modelsWithOutput, target);
158
158
  default:
159
- return renderTsType(type);
159
+ return renderTsType(type, target);
160
160
  }
161
161
  }
162
162
  __name(renderOutputTsType, "renderOutputTsType");
@@ -1200,8 +1200,12 @@ function generateOp(root, options = {}) {
1200
1200
  if (types.length > 0) {
1201
1201
  body.push(...generateTypeImports(types, root.file, options));
1202
1202
  }
1203
- if (opNeedsDateTime(root)) {
1204
- body.push(`import { DateTime } from 'luxon';`);
1203
+ const luxonImports = [];
1204
+ if (opNeedsDateTime(root)) luxonImports.push("DateTime");
1205
+ if (opNeedsScalar(root, "duration")) luxonImports.push("Duration");
1206
+ if (opNeedsScalar(root, "interval")) luxonImports.push("Interval");
1207
+ if (luxonImports.length > 0) {
1208
+ body.push(`import { ${luxonImports.join(", ")} } from 'luxon';`);
1205
1209
  }
1206
1210
  if (needsParseAndValidate) {
1207
1211
  body.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
@@ -1216,6 +1220,9 @@ function generateOp(root, options = {}) {
1216
1220
  if (opNeedsScalar(root, "datetime")) {
1217
1221
  helpers.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' }));`);
1218
1222
  }
1223
+ if (opNeedsScalar(root, "interval")) {
1224
+ helpers.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()!);`);
1225
+ }
1219
1226
  if (opNeedsScalar(root, "json")) {
1220
1227
  helpers.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
1221
1228
  helpers.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)]));`);
@@ -1290,7 +1297,7 @@ function generateHandler(route, op, root, options) {
1290
1297
  middlewares.push(`requireSignature(${sigArgs})`);
1291
1298
  }
1292
1299
  const middlewareStr = middlewares.length > 0 ? `, ${middlewares.join(", ")},` : ",";
1293
- lines.push(`${deriveRouterName(file)}.${method}('${path}'${middlewareStr} async (ctx, next) => {`);
1300
+ lines.push(`${deriveRouterName(file)}.${method}('${path}'${middlewareStr} async ctx => {`);
1294
1301
  lines.push(...generateParamValidation(route.params, "ctx.params", "params", route.paramsMode ?? "strict", "", modelsWithInput));
1295
1302
  lines.push(...generateParamValidation(op.query, "ctx.query", "query", op.queryMode ?? "strict", "", modelsWithInput));
1296
1303
  lines.push(...generateParamValidation(op.headers, "ctx.headers", "headers", op.headersMode ?? "strip", "", modelsWithInput));
@@ -1321,13 +1328,13 @@ function generateHandler(route, op, root, options) {
1321
1328
  lines.push("");
1322
1329
  }
1323
1330
  }
1324
- const primaryResponse = op.responses.find((r) => r.bodyType) ?? op.responses[0];
1331
+ const primaryResponse2 = op.responses.find((r) => r.bodyType) ?? op.responses[0];
1325
1332
  const serviceParts = inferService(op, route, file);
1326
- const respHeaders = primaryResponse?.headers ?? [];
1333
+ const respHeaders = primaryResponse2?.headers ?? [];
1327
1334
  const hasRespHeaders = respHeaders.length > 0;
1328
- const headersAnnotation = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, options.modelsWithOutput)}`).join("; ")} }` : "";
1329
- if (primaryResponse?.bodyType) {
1330
- const { annotation, prelude } = formatTypeAnnotation(primaryResponse.bodyType, options.modelsWithOutput);
1335
+ const headersAnnotation = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, options.modelsWithOutput, "server")}`).join("; ")} }` : "";
1336
+ if (primaryResponse2?.bodyType) {
1337
+ const { annotation, prelude } = formatTypeAnnotation(primaryResponse2.bodyType, options.modelsWithOutput);
1331
1338
  if (prelude) {
1332
1339
  lines.push(` ${prelude}`);
1333
1340
  }
@@ -1346,7 +1353,7 @@ function generateHandler(route, op, root, options) {
1346
1353
  }
1347
1354
  }
1348
1355
  lines.push("");
1349
- lines.push(` ctx.status = ${primaryResponse?.statusCode ?? 200};`);
1356
+ lines.push(` ctx.status = ${primaryResponse2?.statusCode ?? 200};`);
1350
1357
  if (hasRespHeaders) {
1351
1358
  for (const h of respHeaders) {
1352
1359
  const accessor = `result.headers[${JSON.stringify(headerNameToProperty(h.name))}]`;
@@ -1357,8 +1364,8 @@ function generateHandler(route, op, root, options) {
1357
1364
  }
1358
1365
  }
1359
1366
  }
1360
- if (primaryResponse?.bodyType && primaryResponse.contentType) {
1361
- lines.push(` ctx.type = '${primaryResponse.contentType}';`);
1367
+ if (primaryResponse2?.bodyType && primaryResponse2.contentType) {
1368
+ lines.push(` ctx.type = '${primaryResponse2.contentType}';`);
1362
1369
  lines.push(` ctx.body = ${hasRespHeaders ? "result.body" : "result"};`);
1363
1370
  }
1364
1371
  lines.push(`});`);
@@ -1419,6 +1426,45 @@ function buildArgs(route, op) {
1419
1426
  return args.join(", ");
1420
1427
  }
1421
1428
  __name(buildArgs, "buildArgs");
1429
+ function serverTsScalar(name) {
1430
+ switch (name) {
1431
+ case "string":
1432
+ case "email":
1433
+ case "url":
1434
+ case "uuid":
1435
+ return "string";
1436
+ case "number":
1437
+ case "int":
1438
+ return "number";
1439
+ case "bigint":
1440
+ return "bigint";
1441
+ case "boolean":
1442
+ return "boolean";
1443
+ case "date":
1444
+ case "time":
1445
+ case "datetime":
1446
+ return "DateTime";
1447
+ case "duration":
1448
+ return "Duration";
1449
+ case "interval":
1450
+ return "string";
1451
+ case "binary":
1452
+ return "Buffer";
1453
+ case "json":
1454
+ return "_JsonValue";
1455
+ case "object":
1456
+ return "Record<string, unknown>";
1457
+ case "null":
1458
+ return "null";
1459
+ case "unknown":
1460
+ return "unknown";
1461
+ default: {
1462
+ const _exhaustive = name;
1463
+ throw new Error(`plugin-typescript: unmapped scalar '${String(_exhaustive)}' \u2014 add a case`);
1464
+ }
1465
+ }
1466
+ }
1467
+ __name(serverTsScalar, "serverTsScalar");
1422
1468
  function formatTypeAnnotation(bodyType, modelsWithOutput) {
1423
1469
  if (bodyType.kind === "array") {
1424
1470
  const inner = formatTypeAnnotation(bodyType.item, modelsWithOutput);
@@ -1434,7 +1480,7 @@ function formatTypeAnnotation(bodyType, modelsWithOutput) {
1434
1480
  };
1435
1481
  }
1436
1482
  if (bodyType.kind === "scalar") return {
1437
- annotation: bodyType.name
1483
+ annotation: serverTsScalar(bodyType.name)
1438
1484
  };
1439
1485
  const schema = renderType(bodyType);
1440
1486
  return {
@@ -1960,11 +2006,11 @@ function generateMethod(route, op, file, options) {
1960
2006
  const { modelsWithInput, modelsWithOutput } = options;
1961
2007
  const params = buildMethodParams(route, op, modelsWithInput);
1962
2008
  const paramStr = params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ");
1963
- const primaryResponse = op.responses.find((r) => r.bodyType) ?? op.responses[0];
1964
- const isVoid = !primaryResponse?.bodyType;
1965
- const respCategory = primaryResponse?.contentType ? classifyContentType2(primaryResponse.contentType) : "json";
1966
- const dataType = isVoid ? "void" : respCategory === "text" ? "string" : respCategory === "binary" ? "Blob" : renderOutputTsType(primaryResponse.bodyType, modelsWithOutput);
1967
- const respHeaders = primaryResponse?.headers ?? [];
2009
+ const primaryResponse2 = op.responses.find((r) => r.bodyType) ?? op.responses[0];
2010
+ const isVoid = !primaryResponse2?.bodyType;
2011
+ const respCategory = primaryResponse2?.contentType ? classifyContentType2(primaryResponse2.contentType) : "json";
2012
+ const dataType = isVoid ? "void" : respCategory === "text" ? "string" : respCategory === "binary" ? "Blob" : renderOutputTsType(primaryResponse2.bodyType, modelsWithOutput);
2013
+ const respHeaders = primaryResponse2?.headers ?? [];
1968
2014
  const hasRespHeaders = respHeaders.length > 0;
1969
2015
  const headersShape = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`).join("; ")} }` : "";
1970
2016
  const returnType = hasRespHeaders ? isVoid ? `{ headers: ${headersShape} }` : `{ data: ${dataType}; headers: ${headersShape} }` : dataType;
@@ -2807,6 +2853,7 @@ __name(generateSdkAggregator, "generateSdkAggregator");
2807
2853
  import { relative as relative4, dirname as dirname4 } from "path";
2808
2854
  import { computeModelsWithOutput, collectExternalOutputRefs } from "@contractkit/core";
2809
2855
  function generatePlainTypes(root, context) {
2856
+ const target = context?.target ?? "client";
2810
2857
  const externalRefs = collectExternalRefs(root);
2811
2858
  const lines = [];
2812
2859
  const externalModelsWithInput = context?.modelsWithInput ?? /* @__PURE__ */ new Set();
@@ -2848,21 +2895,21 @@ function generatePlainTypes(root, context) {
2848
2895
  m
2849
2896
  ]));
2850
2897
  for (const model of topoSortModels(root.models)) {
2851
- lines.push(...generateModel2(model, context?.currentOutPath, allModelsWithInput, allModelsWithOutput, modelMap));
2898
+ lines.push(...generateModel2(model, target, context?.currentOutPath, allModelsWithInput, allModelsWithOutput, modelMap));
2852
2899
  lines.push("");
2853
2900
  }
2854
2901
  return lines.join("\n");
2855
2902
  }
2856
2903
  __name(generatePlainTypes, "generatePlainTypes");
2857
- function generateModel2(model, outPath, modelsWithInput, modelsWithOutput, modelMap) {
2904
+ function generateModel2(model, target, outPath, modelsWithInput, modelsWithOutput, modelMap) {
2858
2905
  if (model.type) {
2859
- return generateTypeAlias2(model, outPath, modelsWithInput, modelsWithOutput);
2906
+ return generateTypeAlias2(model, target, outPath, modelsWithInput, modelsWithOutput);
2860
2907
  }
2861
2908
  const needsInputSplit = model.fields.some((f) => f.visibility !== "normal") || (modelsWithInput?.has(model.name) ?? false);
2862
- const lines = needsInputSplit ? generateVisibilityModel(model, outPath, modelsWithInput, modelMap) : generateSimpleModel2(model, outPath, modelMap);
2909
+ const lines = needsInputSplit ? generateVisibilityModel(model, target, outPath, modelsWithInput, modelMap) : generateSimpleModel2(model, target, outPath, modelMap);
2863
2910
  if (modelsWithOutput?.has(model.name)) {
2864
2911
  lines.push("");
2865
- lines.push(...generateOutputModel(model, modelsWithOutput));
2912
+ lines.push(...generateOutputModel(model, target, modelsWithOutput));
2866
2913
  }
2867
2914
  return lines;
2868
2915
  }
@@ -2899,15 +2946,15 @@ function generateComments2(model, outPath) {
2899
2946
  return lines;
2900
2947
  }
2901
2948
  __name(generateComments2, "generateComments");
2902
- function generateTypeAlias2(model, outPath, modelsWithInput, modelsWithOutput) {
2949
+ function generateTypeAlias2(model, target, outPath, modelsWithInput, modelsWithOutput) {
2903
2950
  const lines = [];
2904
2951
  lines.push(...generateComments2(model, outPath));
2905
- lines.push(`export type ${model.name} = ${renderTsType(model.type)};`);
2952
+ lines.push(`export type ${model.name} = ${renderTsType(model.type, target)};`);
2906
2953
  if (modelsWithInput?.has(model.name)) {
2907
- lines.push(`export type ${model.name}Input = ${renderInputTsType(model.type, modelsWithInput)};`);
2954
+ lines.push(`export type ${model.name}Input = ${renderInputTsType(model.type, modelsWithInput, target)};`);
2908
2955
  }
2909
2956
  if (modelsWithOutput?.has(model.name)) {
2910
- lines.push(`export type ${model.name}Output = ${renderOutputTsType(model.type, modelsWithOutput)};`);
2957
+ lines.push(`export type ${model.name}Output = ${renderOutputTsType(model.type, modelsWithOutput, target)};`);
2911
2958
  }
2912
2959
  return lines;
2913
2960
  }
@@ -2920,20 +2967,20 @@ function buildExtendsClause(bases, overrideNames, baseNameResolver) {
2920
2967
  return ` extends ${wrapped.join(", ")}`;
2921
2968
  }
2922
2969
  __name(buildExtendsClause, "buildExtendsClause");
2923
- function generateSimpleModel2(model, outPath, modelMap) {
2970
+ function generateSimpleModel2(model, target, outPath, modelMap) {
2924
2971
  const lines = [];
2925
2972
  lines.push(...generateComments2(model, outPath));
2926
2973
  const bases = model.bases ?? [];
2927
2974
  const overrideNames = computeOverrideNames(model, modelMap);
2928
2975
  lines.push(`export interface ${model.name}${buildExtendsClause(bases, overrideNames, (b) => b)} {`);
2929
2976
  for (const field of model.fields) {
2930
- lines.push(` ${renderField2(field)}`);
2977
+ lines.push(` ${renderField2(field, target)}`);
2931
2978
  }
2932
2979
  lines.push("}");
2933
2980
  return lines;
2934
2981
  }
2935
2982
  __name(generateSimpleModel2, "generateSimpleModel");
2936
- function generateVisibilityModel(model, outPath, modelsWithInput, modelMap) {
2983
+ function generateVisibilityModel(model, target, outPath, modelsWithInput, modelMap) {
2937
2984
  const lines = [];
2938
2985
  lines.push(...generateComments2(model, outPath));
2939
2986
  const bases = model.bases ?? [];
@@ -2941,7 +2988,7 @@ function generateVisibilityModel(model, outPath, modelsWithInput, modelMap) {
2941
2988
  const readFields = model.fields.filter((f) => f.visibility !== "writeonly");
2942
2989
  lines.push(`export interface ${model.name}${buildExtendsClause(bases, overrideNames, (b) => b)} {`);
2943
2990
  for (const field of readFields) {
2944
- lines.push(` ${renderField2(field)}`);
2991
+ lines.push(` ${renderField2(field, target)}`);
2945
2992
  }
2946
2993
  lines.push("}");
2947
2994
  lines.push("");
@@ -2949,7 +2996,7 @@ function generateVisibilityModel(model, outPath, modelsWithInput, modelMap) {
2949
2996
  const inputResolver = /* @__PURE__ */ __name((b) => modelsWithInput?.has(b) ? `${b}Input` : b, "inputResolver");
2950
2997
  lines.push(`export interface ${model.name}Input${buildExtendsClause(bases, overrideNames, inputResolver)} {`);
2951
2998
  for (const field of writeFields) {
2952
- lines.push(` ${modelsWithInput ? renderInputField2(field, modelsWithInput) : renderField2(field)}`);
2999
+ lines.push(` ${modelsWithInput ? renderInputField2(field, modelsWithInput, target) : renderField2(field, target)}`);
2953
3000
  }
2954
3001
  lines.push("}");
2955
3002
  return lines;
@@ -2969,9 +3016,9 @@ ${body}
2969
3016
  ${line}`;
2970
3017
  }
2971
3018
  __name(withFieldJsDoc, "withFieldJsDoc");
2972
- function renderField2(field) {
3019
+ function renderField2(field, target) {
2973
3020
  const opt = field.optional || field.default !== void 0 ? "?" : "";
2974
- let typeStr = renderTsType(field.type);
3021
+ let typeStr = renderTsType(field.type, target);
2975
3022
  if (field.nullable) typeStr += " | null";
2976
3023
  const line = `${quoteKey(field.name)}${opt}: ${typeStr};`;
2977
3024
  const jsdocParts = [];
@@ -2980,9 +3027,9 @@ function renderField2(field) {
2980
3027
  return withFieldJsDoc(jsdocParts, line);
2981
3028
  }
2982
3029
  __name(renderField2, "renderField");
2983
- function renderInputField2(field, modelsWithInput) {
3030
+ function renderInputField2(field, modelsWithInput, target) {
2984
3031
  const opt = field.optional || field.default !== void 0 ? "?" : "";
2985
- let typeStr = renderInputTsType(field.type, modelsWithInput);
3032
+ let typeStr = renderInputTsType(field.type, modelsWithInput, target);
2986
3033
  if (field.nullable) typeStr += " | null";
2987
3034
  const line = `${quoteKey(field.name)}${opt}: ${typeStr};`;
2988
3035
  const jsdocParts = [];
@@ -3005,7 +3052,7 @@ function applyOutputCase(name, c) {
3005
3052
  return camelToPascal2(name);
3006
3053
  }
3007
3054
  __name(applyOutputCase, "applyOutputCase");
3008
- function generateOutputModel(model, modelsWithOutput) {
3055
+ function generateOutputModel(model, target, modelsWithOutput) {
3009
3056
  const lines = [];
3010
3057
  const outputCase = model.outputCase && model.outputCase !== "camel" ? model.outputCase : void 0;
3011
3058
  const readFields = model.fields.filter((f) => f.visibility !== "writeonly");
@@ -3013,23 +3060,23 @@ function generateOutputModel(model, modelsWithOutput) {
3013
3060
  const baseExt = model.bases?.[0] && modelsWithOutput.has(model.bases?.[0]) ? ` extends ${model.bases?.[0]}Output` : model.bases?.[0] ? ` extends ${model.bases?.[0]}` : "";
3014
3061
  lines.push(`export interface ${model.name}Output${baseExt} {`);
3015
3062
  for (const field of readFields) {
3016
- lines.push(` ${renderOutputField(field, model.outputCase, modelsWithOutput)}`);
3063
+ lines.push(` ${renderOutputField(field, model.outputCase, modelsWithOutput, target)}`);
3017
3064
  }
3018
3065
  lines.push("}");
3019
3066
  return lines;
3020
3067
  }
3021
3068
  lines.push(`export interface ${model.name}Output {`);
3022
3069
  for (const field of readFields) {
3023
- lines.push(` ${renderOutputField(field, outputCase, modelsWithOutput)}`);
3070
+ lines.push(` ${renderOutputField(field, outputCase, modelsWithOutput, target)}`);
3024
3071
  }
3025
3072
  lines.push("}");
3026
3073
  return lines;
3027
3074
  }
3028
3075
  __name(generateOutputModel, "generateOutputModel");
3029
- function renderOutputField(field, outputCase, modelsWithOutput) {
3076
+ function renderOutputField(field, outputCase, modelsWithOutput, target) {
3030
3077
  const opt = field.optional || field.default !== void 0 ? "?" : "";
3031
3078
  const key = applyOutputCase(field.name, outputCase);
3032
- let typeStr = renderOutputTsType(field.type, modelsWithOutput);
3079
+ let typeStr = renderOutputTsType(field.type, modelsWithOutput, target);
3033
3080
  if (field.nullable) typeStr += " | null";
3034
3081
  const line = `${quoteKey(key)}${opt}: ${typeStr};`;
3035
3082
  const jsdocParts = [];
@@ -3039,12 +3086,443 @@ function renderOutputField(field, outputCase, modelsWithOutput) {
3039
3086
  }
3040
3087
  __name(renderOutputField, "renderOutputField");
3041
3088
 
3089
+ // src/codegen-mcp.ts
3090
+ import { resolveModifiers as resolveModifiers3 } from "@contractkit/core";
3091
+ import { basename as basename3, dirname as dirname5, relative as relative5 } from "path";
3092
+ function mcpConfig(op) {
3093
+ return op.mcp && typeof op.mcp === "object" ? op.mcp : void 0;
3094
+ }
3095
+ __name(mcpConfig, "mcpConfig");
3096
+ function hasMcpOperations(root, includeInternal = false) {
3097
+ for (const route of root.routes) {
3098
+ for (const op of route.operations) {
3099
+ if (!op.mcp) continue;
3100
+ if (!includeInternal && resolveModifiers3(route, op).includes("internal")) continue;
3101
+ return true;
3102
+ }
3103
+ }
3104
+ return false;
3105
+ }
3106
+ __name(hasMcpOperations, "hasMcpOperations");
3107
+ function toSnake(s) {
3108
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[\s\-.]+/g, "_").toLowerCase().replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_|_$/g, "");
3109
+ }
3110
+ __name(toSnake, "toSnake");
3111
+ function toPascal(s) {
3112
+ return s.split("_").filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
3113
+ }
3114
+ __name(toPascal, "toPascal");
3115
+ function inferToolName(method, path) {
3116
+ const parts = [
3117
+ method.toLowerCase()
3118
+ ];
3119
+ for (const seg of path.split("/").filter(Boolean)) {
3120
+ if (seg.startsWith("{")) {
3121
+ parts.push("by", toSnake(seg.slice(1, -1)));
3122
+ } else {
3123
+ parts.push(toSnake(seg));
3124
+ }
3125
+ }
3126
+ return parts.filter(Boolean).join("_");
3127
+ }
3128
+ __name(inferToolName, "inferToolName");
3129
+ function deriveToolName(op, route) {
3130
+ const cfg = mcpConfig(op);
3131
+ if (cfg?.name) return cfg.name;
3132
+ if (op.sdk) return toSnake(op.sdk);
3133
+ if (op.name) return toSnake(op.name);
3134
+ return inferToolName(op.method, route.path);
3135
+ }
3136
+ __name(deriveToolName, "deriveToolName");
3137
+ function deriveToolClassName(toolName) {
3138
+ return `${toPascal(toolName)}McpTool`;
3139
+ }
3140
+ __name(deriveToolClassName, "deriveToolClassName");
3141
+ function buildArgsProps(route, op, modelsWithInput) {
3142
+ const props = [];
3143
+ if (route.params) {
3144
+ if (route.params.kind === "params") {
3145
+ for (const node of route.params.nodes) {
3146
+ props.push({
3147
+ key: node.name,
3148
+ expr: renderInputType(node.type, modelsWithInput),
3149
+ optional: false
3150
+ });
3151
+ }
3152
+ } else if (route.params.kind === "ref") {
3153
+ props.push({
3154
+ key: "params",
3155
+ expr: refSchema(route.params.name, modelsWithInput),
3156
+ optional: false
3157
+ });
3158
+ } else {
3159
+ props.push({
3160
+ key: "params",
3161
+ expr: renderInputType(route.params.node, modelsWithInput),
3162
+ optional: false
3163
+ });
3164
+ }
3165
+ }
3166
+ const bodies = op.request?.bodies ?? [];
3167
+ if (bodies.length === 1 && bodies[0].contentType === "multipart/form-data") {
3168
+ props.push({
3169
+ key: "multipartBody",
3170
+ expr: "z.unknown()",
3171
+ optional: false
3172
+ });
3173
+ } else if (bodies.length === 1) {
3174
+ props.push({
3175
+ key: "body",
3176
+ expr: renderInputType(bodies[0].bodyType, modelsWithInput),
3177
+ optional: false
3178
+ });
3179
+ } else if (bodies.length > 1) {
3180
+ props.push({
3181
+ key: "body",
3182
+ expr: "z.unknown()",
3183
+ optional: false
3184
+ });
3185
+ }
3186
+ if (op.query) props.push({
3187
+ key: "query",
3188
+ expr: paramSourceSchema(op.query, modelsWithInput),
3189
+ optional: true
3190
+ });
3191
+ if (op.headers) props.push({
3192
+ key: "headers",
3193
+ expr: paramSourceSchema(op.headers, modelsWithInput),
3194
+ optional: true
3195
+ });
3196
+ return props;
3197
+ }
3198
+ __name(buildArgsProps, "buildArgsProps");
3199
+ function refSchema(name, modelsWithInput) {
3200
+ return modelsWithInput?.has(name) ? `${name}Input` : name;
3201
+ }
3202
+ __name(refSchema, "refSchema");
3203
+ function paramSourceSchema(src, modelsWithInput) {
3204
+ if (src.kind === "ref") return refSchema(src.name, modelsWithInput);
3205
+ if (src.kind === "type") return renderInputType(src.node, modelsWithInput);
3206
+ const fields = src.nodes.map((n) => `${quoteKey(n.name)}: ${renderInputType(n.type, modelsWithInput)}`).join(", ");
3207
+ return `z.object({ ${fields} })`;
3208
+ }
3209
+ __name(paramSourceSchema, "paramSourceSchema");
3210
+ function argsSchemaExpr(props) {
3211
+ if (props.length === 0) return "z.object({})";
3212
+ const fields = props.map((p) => `${quoteKey(p.key)}: ${p.expr}${p.optional ? ".optional()" : ""}`).join(", ");
3213
+ return `z.object({ ${fields} })`;
3214
+ }
3215
+ __name(argsSchemaExpr, "argsSchemaExpr");
3216
+ function primaryResponse(op) {
3217
+ return op.responses.find((r) => r.bodyType) ?? op.responses[0];
3218
+ }
3219
+ __name(primaryResponse, "primaryResponse");
3220
+ function outputSchemaExpr(op) {
3221
+ const body = primaryResponse(op)?.bodyType;
3222
+ if (!body) return void 0;
3223
+ if (body.kind === "ref") return body.name;
3224
+ if (body.kind === "inlineObject") return renderType(body);
3225
+ return void 0;
3226
+ }
3227
+ __name(outputSchemaExpr, "outputSchemaExpr");
3228
+ var HINT_KEYS = [
3229
+ "readOnlyHint",
3230
+ "destructiveHint",
3231
+ "idempotentHint",
3232
+ "openWorldHint"
3233
+ ];
3234
+ function annotationsExpr(cfg) {
3235
+ if (!cfg) return void 0;
3236
+ const parts = [];
3237
+ for (const key of HINT_KEYS) {
3238
+ const val = cfg[key];
3239
+ if (val !== void 0) parts.push(`${key}: ${val}`);
3240
+ }
3241
+ return parts.length > 0 ? `{ ${parts.join(", ")} }` : void 0;
3242
+ }
3243
+ __name(annotationsExpr, "annotationsExpr");
3244
+ function walkTypeRefs(type, ids, variant, modelsWithInput) {
3245
+ switch (type.kind) {
3246
+ case "ref":
3247
+ ids.add(variant === "input" ? refSchema(type.name, modelsWithInput) : type.name);
3248
+ break;
3249
+ case "array":
3250
+ walkTypeRefs(type.item, ids, variant, modelsWithInput);
3251
+ break;
3252
+ case "tuple":
3253
+ type.items.forEach((t) => walkTypeRefs(t, ids, variant, modelsWithInput));
3254
+ break;
3255
+ case "record":
3256
+ walkTypeRefs(type.key, ids, variant, modelsWithInput);
3257
+ walkTypeRefs(type.value, ids, variant, modelsWithInput);
3258
+ break;
3259
+ case "union":
3260
+ case "discriminatedUnion":
3261
+ case "intersection":
3262
+ type.members.forEach((t) => walkTypeRefs(t, ids, variant, modelsWithInput));
3263
+ break;
3264
+ case "inlineObject":
3265
+ type.fields.forEach((f) => walkTypeRefs(f.type, ids, variant, modelsWithInput));
3266
+ break;
3267
+ case "lazy":
3268
+ walkTypeRefs(type.inner, ids, variant, modelsWithInput);
3269
+ break;
3270
+ }
3271
+ }
3272
+ __name(walkTypeRefs, "walkTypeRefs");
3273
+ function walkSourceRefs(src, ids, modelsWithInput) {
3274
+ if (!src) return;
3275
+ if (src.kind === "ref") ids.add(refSchema(src.name, modelsWithInput));
3276
+ else if (src.kind === "params") src.nodes.forEach((n) => walkTypeRefs(n.type, ids, "input", modelsWithInput));
3277
+ else walkTypeRefs(src.node, ids, "input", modelsWithInput);
3278
+ }
3279
+ __name(walkSourceRefs, "walkSourceRefs");
3280
+ function collectSchemaIds(ops, modelsWithInput) {
3281
+ const ids = /* @__PURE__ */ new Set();
3282
+ for (const { route, op } of ops) {
3283
+ walkSourceRefs(route.params, ids, modelsWithInput);
3284
+ const bodies = op.request?.bodies ?? [];
3285
+ if (bodies.length === 1 && bodies[0].contentType !== "multipart/form-data") {
3286
+ walkTypeRefs(bodies[0].bodyType, ids, "input", modelsWithInput);
3287
+ }
3288
+ walkSourceRefs(op.query, ids, modelsWithInput);
3289
+ walkSourceRefs(op.headers, ids, modelsWithInput);
3290
+ const body = primaryResponse(op)?.bodyType;
3291
+ if (body && (body.kind === "ref" || body.kind === "inlineObject")) walkTypeRefs(body, ids, "read");
3292
+ }
3293
+ return ids;
3294
+ }
3295
+ __name(collectSchemaIds, "collectSchemaIds");
3296
+ function schemaImportLines(ids, options) {
3297
+ const lines = [];
3298
+ const { modelOutPaths, outPath } = options;
3299
+ if (ids.size === 0) return lines;
3300
+ if (modelOutPaths && outPath) {
3301
+ const byFile = /* @__PURE__ */ new Map();
3302
+ const unresolved = [];
3303
+ for (const id of ids) {
3304
+ const p = modelOutPaths.get(id);
3305
+ if (p) {
3306
+ const group = byFile.get(p) ?? [];
3307
+ group.push(id);
3308
+ byFile.set(p, group);
3309
+ } else {
3310
+ unresolved.push(id);
3311
+ }
3312
+ }
3313
+ const fromDir = dirname5(outPath);
3314
+ for (const [file, names] of byFile) {
3315
+ let rel = relative5(fromDir, file).replace(/\.ts$/, ".js");
3316
+ if (!rel.startsWith(".")) rel = "./" + rel;
3317
+ lines.push(`import { ${names.sort().join(", ")} } from '${rel}';`);
3318
+ }
3319
+ for (const id of unresolved.sort()) lines.push(`import { ${id} } from './${pascalToDotCase(id)}.js';`);
3320
+ } else {
3321
+ for (const id of [
3322
+ ...ids
3323
+ ].sort()) lines.push(`import { ${id} } from './${pascalToDotCase(id)}.js';`);
3324
+ }
3325
+ return lines;
3326
+ }
3327
+ __name(schemaImportLines, "schemaImportLines");
3328
+ function scalarHelperLines(body) {
3329
+ const lines = [];
3330
+ if (body.includes("_ZodBinary")) {
3331
+ lines.push(`const _ZodBinary = z.custom<Buffer>((val) => Buffer.isBuffer(val), { error: 'Must be binary data' });`);
3332
+ }
3333
+ if (body.includes("_ZodDatetime")) {
3334
+ 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' }));`);
3335
+ }
3336
+ if (body.includes("_ZodInterval")) {
3337
+ 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()!);`);
3338
+ }
3339
+ if (body.includes("_ZodJson")) {
3340
+ lines.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
3341
+ 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)]));`);
3342
+ }
3343
+ return lines;
3344
+ }
3345
+ __name(scalarHelperLines, "scalarHelperLines");
3346
+ function planTools(root, includeInternal) {
3347
+ const plans = [];
3348
+ for (const route of root.routes) {
3349
+ for (const op of route.operations) {
3350
+ if (!op.mcp) continue;
3351
+ if (!includeInternal && resolveModifiers3(route, op).includes("internal")) continue;
3352
+ const toolName = deriveToolName(op, route);
3353
+ const className = deriveToolClassName(toolName);
3354
+ plans.push({
3355
+ route,
3356
+ op,
3357
+ toolName,
3358
+ className,
3359
+ argsConstName: `${toPascal(toolName)}Args`
3360
+ });
3361
+ }
3362
+ }
3363
+ return plans;
3364
+ }
3365
+ __name(planTools, "planTools");
3366
+ function renderToolClass(plan, file, options) {
3367
+ const { route, op, toolName, className, argsConstName } = plan;
3368
+ const cfg = mcpConfig(op);
3369
+ const lines = [];
3370
+ const relFile = options.outPath ? relative5(dirname5(options.outPath), file) : file;
3371
+ lines.push("/**");
3372
+ lines.push(` * from [${basename3(file)}](file://./${relFile}#L${op.loc.line})`);
3373
+ lines.push(" */");
3374
+ lines.push("@Injectable()");
3375
+ lines.push(`export class ${className} implements McpToolHandler {`);
3376
+ lines.push(" readonly definition: Tool = {");
3377
+ lines.push(` name: '${escapeSingleQuoted(toolName)}',`);
3378
+ if (cfg?.title) lines.push(` title: '${escapeSingleQuoted(cfg.title)}',`);
3379
+ const desc = cfg?.description ?? op.description ?? route.description;
3380
+ if (desc) lines.push(` description: '${escapeSingleQuoted(desc)}',`);
3381
+ lines.push(` inputSchema: z.toJSONSchema(${argsConstName}, { unrepresentable: 'any' }) as Tool['inputSchema'],`);
3382
+ const outExpr = outputSchemaExpr(op);
3383
+ if (outExpr) lines.push(` outputSchema: z.toJSONSchema(${outExpr}, { unrepresentable: 'any' }) as Tool['outputSchema'],`);
3384
+ const annotations = annotationsExpr(cfg);
3385
+ if (annotations) lines.push(` annotations: ${annotations},`);
3386
+ lines.push(" };");
3387
+ lines.push("");
3388
+ const service = inferService(op, route, file);
3389
+ lines.push(` constructor(private readonly service: ${service.className}) {}`);
3390
+ lines.push("");
3391
+ const props = buildArgsProps(route, op, options.modelsWithInput);
3392
+ const destructure = props.map((p) => p.key);
3393
+ const callArgs = buildArgs(route, op);
3394
+ const isVoid = !primaryResponse(op)?.bodyType;
3395
+ const structured = !!outExpr;
3396
+ lines.push(" async handle(args: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {");
3397
+ if (destructure.length > 0) {
3398
+ lines.push(` const { ${destructure.join(", ")} } = await parseAndValidate(args, ${argsConstName});`);
3399
+ }
3400
+ if (isVoid) {
3401
+ lines.push(` await this.service.${service.methodName}(${callArgs});`);
3402
+ lines.push(` return { content: [{ type: 'text', text: 'OK' }] };`);
3403
+ } else {
3404
+ lines.push(` const result = await this.service.${service.methodName}(${callArgs});`);
3405
+ if (structured) {
3406
+ lines.push(` return { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result };`);
3407
+ } else {
3408
+ lines.push(` return { content: [{ type: 'text', text: JSON.stringify(result) }] };`);
3409
+ }
3410
+ }
3411
+ lines.push(" }");
3412
+ lines.push("}");
3413
+ return lines;
3414
+ }
3415
+ __name(renderToolClass, "renderToolClass");
3416
+ function deriveMcpRegisterFnName(file) {
3417
+ return `register${deriveBaseName(file)}McpTools`;
3418
+ }
3419
+ __name(deriveMcpRegisterFnName, "deriveMcpRegisterFnName");
3420
+ function generateMcpFile(root, options = {}) {
3421
+ const includeInternal = options.includeInternal ?? false;
3422
+ const plans = planTools(root, includeInternal);
3423
+ const argsConsts = plans.map((p) => `const ${p.argsConstName} = ${argsSchemaExpr(buildArgsProps(p.route, p.op, options.modelsWithInput))};`);
3424
+ const classes = plans.map((p) => renderToolClass(p, root.file, options).join("\n"));
3425
+ const registerFn = [];
3426
+ registerFn.push(`/** Add this file's tools to the shared catalog. */`);
3427
+ registerFn.push(`export function ${deriveMcpRegisterFnName(root.file)}(map: McpToolHandlerMap, container: Container): void {`);
3428
+ for (const p of plans) registerFn.push(` map.set('${escapeSingleQuoted(p.toolName)}', container.get(${p.className}));`);
3429
+ registerFn.push("}");
3430
+ const bodyCore = [
3431
+ argsConsts.join("\n"),
3432
+ classes.join("\n\n"),
3433
+ registerFn.join("\n")
3434
+ ].filter(Boolean).join("\n\n");
3435
+ const helperConsts = scalarHelperLines(bodyCore);
3436
+ const bodyWithHelpers = [
3437
+ helperConsts.join("\n"),
3438
+ bodyCore
3439
+ ].filter(Boolean).join("\n\n");
3440
+ const needsParseAndValidate = plans.some((p) => buildArgsProps(p.route, p.op, options.modelsWithInput).length > 0);
3441
+ const imports = [];
3442
+ imports.push(`import { Injectable, type Container } from 'injectkit';`);
3443
+ imports.push(`import { z } from 'zod';`);
3444
+ const luxon = [];
3445
+ if (/\bDateTime\b/.test(bodyWithHelpers)) luxon.push("DateTime");
3446
+ if (/\bInterval\b/.test(bodyWithHelpers)) luxon.push("Interval");
3447
+ if (/\bDuration\b/.test(bodyWithHelpers)) luxon.push("Duration");
3448
+ if (luxon.length > 0) imports.push(`import { ${luxon.join(", ")} } from 'luxon';`);
3449
+ imports.push(`import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';`);
3450
+ imports.push(`import type { McpToolHandler, McpToolHandlerMap, McpToolContext } from '@maroonedsoftware/mcp';`);
3451
+ if (needsParseAndValidate) imports.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
3452
+ const serviceModules = /* @__PURE__ */ new Map();
3453
+ for (const p of plans) {
3454
+ const svc = inferService(p.op, p.route, root.file).className;
3455
+ if (!serviceModules.has(svc)) {
3456
+ serviceModules.set(svc, root.services?.[svc] ?? root.meta[svc] ?? deriveModulePath(svc, options.servicePathTemplate));
3457
+ }
3458
+ }
3459
+ for (const [svc, mod] of [
3460
+ ...serviceModules.entries()
3461
+ ].sort(([a], [b]) => a.localeCompare(b))) {
3462
+ imports.push(`import { ${svc} } from '${mod}';`);
3463
+ }
3464
+ imports.push(...schemaImportLines(collectSchemaIds(plans, options.modelsWithInput), options));
3465
+ const relFile = options.outPath ? relative5(dirname5(options.outPath), root.file) : root.file;
3466
+ const header = `// Auto-generated MCP tools
3467
+ // generated from [${basename3(root.file)}](file://./${relFile})`;
3468
+ return `${header}
3469
+ ${imports.join("\n")}
3470
+
3471
+ ${bodyWithHelpers}
3472
+ `;
3473
+ }
3474
+ __name(generateMcpFile, "generateMcpFile");
3475
+ function generateMcpAggregator(entries) {
3476
+ const sorted = [
3477
+ ...entries
3478
+ ].sort((a, b) => a.registerFn.localeCompare(b.registerFn));
3479
+ const lines = [];
3480
+ lines.push(`import { type Container } from 'injectkit';`);
3481
+ lines.push(`import { McpToolHandlerMap } from '@maroonedsoftware/mcp';`);
3482
+ for (const e of sorted) lines.push(`import { ${e.registerFn} } from '${e.importPath}';`);
3483
+ lines.push("");
3484
+ lines.push("/** Build + register the MCP tool catalog. Call once at startup. */");
3485
+ lines.push("export function registerMcpTools(container: Container): McpToolHandlerMap {");
3486
+ lines.push(" const map = new McpToolHandlerMap();");
3487
+ for (const e of sorted) lines.push(` ${e.registerFn}(map, container);`);
3488
+ lines.push(" container.register(McpToolHandlerMap, { useValue: map });");
3489
+ lines.push(" return map;");
3490
+ lines.push("}");
3491
+ return lines.join("\n") + "\n";
3492
+ }
3493
+ __name(generateMcpAggregator, "generateMcpAggregator");
3494
+ function generateMcpRouter(options = {}) {
3495
+ const path = options.path ?? "/mcp";
3496
+ return `import { ServerKitRouter, requireSignature } from '@maroonedsoftware/koa';
3497
+ import { McpDispatcher, createMcpRequestContext, MCP_AUTH_POLICY } from '@maroonedsoftware/mcp';
3498
+
3499
+ /** Mount the MCP endpoint onto a ServerKit router. Call \`registerMcpTools(container)\` at startup. */
3500
+ export function mountMcp(router: ReturnType<typeof ServerKitRouter>): void {
3501
+ router.post('${path}', requireSignature('mcp', { policy: MCP_AUTH_POLICY }), async (ctx) => {
3502
+ const dispatcher = ctx.container.get(McpDispatcher);
3503
+ const context = createMcpRequestContext({ requestId: ctx.requestId, logger: ctx.logger });
3504
+ if (dispatcher.sessionMode === 'stateful') {
3505
+ ctx.respond = false;
3506
+ await dispatcher.dispatchStateful(
3507
+ { req: ctx.req, res: ctx.res, body: ctx.request.body, sessionId: ctx.get('mcp-session-id') },
3508
+ context,
3509
+ );
3510
+ } else {
3511
+ const response = await dispatcher.dispatch(JSON.parse(ctx.rawBody), context);
3512
+ if (response) ctx.body = response;
3513
+ }
3514
+ });
3515
+ }
3516
+ `;
3517
+ }
3518
+ __name(generateMcpRouter, "generateMcpRouter");
3519
+
3042
3520
  // src/path-utils.ts
3043
- import { resolve, join, relative as relative5, dirname as dirname5, isAbsolute } from "path";
3521
+ import { resolve, join, relative as relative6, dirname as dirname6, isAbsolute } from "path";
3044
3522
  import { collectTypeRefs as collectTypeRefs2, collectPublicTypeNames } from "@contractkit/core";
3045
3523
  var TEMPLATE_VAR_RE = /\{\w+\}/;
3046
3524
  function assertWithinBase(baseOutDir, outPath) {
3047
- const rel = relative5(resolve(baseOutDir), resolve(outPath));
3525
+ const rel = relative6(resolve(baseOutDir), resolve(outPath));
3048
3526
  if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
3049
3527
  throw new Error(`Refusing to emit outside output directory: resolved path "${outPath}" escapes "${baseOutDir}" (check options { keys } values used in output path templates)`);
3050
3528
  }
@@ -3062,7 +3540,7 @@ function includesFilename(p) {
3062
3540
  __name(includesFilename, "includesFilename");
3063
3541
  function commonDir(files, rootDir) {
3064
3542
  if (files.length === 0) return resolve(rootDir);
3065
- const parts = files.map((f) => dirname5(f).split("/"));
3543
+ const parts = files.map((f) => dirname6(f).split("/"));
3066
3544
  const first = parts[0];
3067
3545
  let depth = first.length;
3068
3546
  for (const p of parts) {
@@ -3078,7 +3556,7 @@ function commonDir(files, rootDir) {
3078
3556
  __name(commonDir, "commonDir");
3079
3557
  function computeOpOutPath(filePath, baseDir, output, defaultSuffix, commonRoot, meta = {}) {
3080
3558
  const baseName = filePath.split("/").pop();
3081
- const relDir = relative5(commonRoot, dirname5(filePath));
3559
+ const relDir = relative6(commonRoot, dirname6(filePath));
3082
3560
  const filename = baseName.replace(/\.ck$/, "");
3083
3561
  const defaultName = `${filename}${defaultSuffix}`;
3084
3562
  const baseOutDir = resolve(baseDir);
@@ -3108,7 +3586,7 @@ function computeSdkOutPath(filePath, rootDir, clientOutput, commonRoot, meta = {
3108
3586
  const baseName = filePath.split("/").pop();
3109
3587
  const defaultOutName = baseName.replace(/\.ck$/, ".client.ts");
3110
3588
  const baseOutDir = resolve(rootDir);
3111
- const relDir = relative5(commonRoot, dirname5(filePath));
3589
+ const relDir = relative6(commonRoot, dirname6(filePath));
3112
3590
  const filename = baseName.replace(/\.ck$/, "");
3113
3591
  if (clientOutput && TEMPLATE_VAR_RE.test(clientOutput)) {
3114
3592
  const resolved = resolveTemplate(clientOutput, {
@@ -3160,7 +3638,7 @@ function computeSdkTypeOutPath(filePath, rootDir, typeOutput, commonRoot, meta =
3160
3638
  const baseName = filePath.split("/").pop();
3161
3639
  const defaultOutName = baseName.replace(/\.ck$/, ".ts");
3162
3640
  const baseOutDir = resolve(rootDir);
3163
- const relDir = relative5(commonRoot, dirname5(filePath));
3641
+ const relDir = relative6(commonRoot, dirname6(filePath));
3164
3642
  const filename = baseName.replace(/\.ck$/, "");
3165
3643
  if (TEMPLATE_VAR_RE.test(typeOutput)) {
3166
3644
  const resolved = resolveTemplate(typeOutput, {
@@ -3179,7 +3657,7 @@ __name(computeSdkTypeOutPath, "computeSdkTypeOutPath");
3179
3657
  function generateBarrelFiles(contractPaths) {
3180
3658
  const byDir = /* @__PURE__ */ new Map();
3181
3659
  for (const outPath of contractPaths) {
3182
- const dir = dirname5(outPath);
3660
+ const dir = dirname6(outPath);
3183
3661
  const group = byDir.get(dir) ?? [];
3184
3662
  group.push(outPath);
3185
3663
  byDir.set(dir, group);
@@ -3273,6 +3751,7 @@ async function runTypescriptCodegen(inputs, ctx, config, rootDir) {
3273
3751
  if (config.sdk) collectSdkOutput(config.sdk, rootDir, inputs, units, globalFiles);
3274
3752
  if (config.zod) collectZodOutput(config.zod, rootDir, inputs, units);
3275
3753
  if (config.types) collectTypesOutput(config.types, rootDir, inputs, units);
3754
+ if (config.mcp) collectMcpOutput(config.mcp, config, rootDir, inputs, units, globalFiles);
3276
3755
  const result = runIncrementalCodegen({
3277
3756
  codegenVersion: TYPESCRIPT_CODEGEN_VERSION,
3278
3757
  prevManifest,
@@ -3424,7 +3903,9 @@ function collectServerOutput(config, rootDir, inputs, units) {
3424
3903
  modelOutPaths: serverModelOutPaths,
3425
3904
  currentOutPath: typeOutPath,
3426
3905
  modelsWithInput,
3427
- modelsWithOutput
3906
+ modelsWithOutput,
3907
+ // These types are consumed by Koa handlers, so `binary` is a Buffer, not a Blob.
3908
+ target: "server"
3428
3909
  };
3429
3910
  const content = config.zod ? generateContract(ast, renderCtx) : generatePlainTypes(ast, renderCtx);
3430
3911
  return [
@@ -3479,7 +3960,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3479
3960
  const sdkEntryPath = sdkOutput ? join2(sdkBase, TEMPLATE_VAR_RE.test(sdkOutput) ? resolveTemplate(sdkOutput, {
3480
3961
  name: sdkName ?? "sdk"
3481
3962
  }) : sdkOutput) : join2(sdkBase, "sdk.ts");
3482
- const sdkOptionsPath = join2(dirname6(sdkEntryPath), "sdk-options.ts");
3963
+ const sdkOptionsPath = join2(dirname7(sdkEntryPath), "sdk-options.ts");
3483
3964
  const subConfigKey = stableSubConfig(config);
3484
3965
  const modelsWithInput = inputs.modelsWithInput;
3485
3966
  const modelsWithOutput = inputs.modelsWithOutput;
@@ -3538,7 +4019,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3538
4019
  modelsWithOutput
3539
4020
  });
3540
4021
  } else {
3541
- let rel = relative6(dirname6(typeOutPath), sdkOptionsPath).replace(/\.ts$/, ".js");
4022
+ let rel = relative7(dirname7(typeOutPath), sdkOptionsPath).replace(/\.ts$/, ".js");
3542
4023
  if (!rel.startsWith(".")) rel = "./" + rel;
3543
4024
  content = generatePlainTypes(ast, {
3544
4025
  modelOutPaths: sdkModelOutPaths,
@@ -3679,12 +4160,12 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3679
4160
  const hasAnything = sdkClientInfos.length > 0 || areaBuckets.size > 0;
3680
4161
  const areaClientOutPaths = /* @__PURE__ */ new Map();
3681
4162
  if (hasAnything) {
3682
- const sdkEntryDir = dirname6(sdkEntryPath);
3683
- const sdkOptionsRel = relative6(sdkEntryDir, sdkOptionsPath).replace(/\.ts$/, ".js");
4163
+ const sdkEntryDir = dirname7(sdkEntryPath);
4164
+ const sdkOptionsRel = relative7(sdkEntryDir, sdkOptionsPath).replace(/\.ts$/, ".js");
3684
4165
  const sdkOptionsImportPath = sdkOptionsRel.startsWith(".") ? sdkOptionsRel : "./" + sdkOptionsRel;
3685
4166
  const sdkClassName = sdkName ? sdkName.split(/[-._\s]+/).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Sdk" : "Sdk";
3686
4167
  const toClientImport = /* @__PURE__ */ __name((sourceDir, info) => {
3687
- let rel = relative6(sourceDir, info.outPath).replace(/\.ts$/, ".js");
4168
+ let rel = relative7(sourceDir, info.outPath).replace(/\.ts$/, ".js");
3688
4169
  if (!rel.startsWith(".")) rel = "./" + rel;
3689
4170
  return {
3690
4171
  className: info.className,
@@ -3713,7 +4194,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3713
4194
  });
3714
4195
  const subareaClients = bucket.leaves.sort((a, b) => a.subarea.localeCompare(b.subarea)).map((l) => ({
3715
4196
  propertyName: deriveSubareaPropertyName(l.subarea),
3716
- client: toClientImport(dirname6(areaClientOutPath), {
4197
+ client: toClientImport(dirname7(areaClientOutPath), {
3717
4198
  outPath: l.outPath,
3718
4199
  className: deriveSubareaClientClassName(area, l.subarea),
3719
4200
  propertyName: deriveSubareaPropertyName(l.subarea)
@@ -3784,23 +4265,23 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3784
4265
  })
3785
4266
  });
3786
4267
  }
3787
- const sdkSrcDir = dirname6(sdkEntryPath);
4268
+ const sdkSrcDir = dirname7(sdkEntryPath);
3788
4269
  const sdkTypeBarrels = generateBarrelFiles(sdkTypePaths);
3789
4270
  for (const barrel of sdkTypeBarrels) globalFiles.push({
3790
4271
  relativePath: barrel.outPath,
3791
4272
  content: barrel.content
3792
4273
  });
3793
4274
  const rootExports = [
3794
- `export * from './${basename3(sdkOptionsPath).replace(/\.ts$/, ".js")}';`
4275
+ `export * from './${basename4(sdkOptionsPath).replace(/\.ts$/, ".js")}';`
3795
4276
  ];
3796
- if (hasAnything) rootExports.push(`export * from './${basename3(sdkEntryPath).replace(/\.ts$/, ".js")}';`);
4277
+ if (hasAnything) rootExports.push(`export * from './${basename4(sdkEntryPath).replace(/\.ts$/, ".js")}';`);
3797
4278
  for (const c of sdkClientInfos) {
3798
- let rel = relative6(sdkSrcDir, c.outPath).replace(/\.ts$/, ".js");
4279
+ let rel = relative7(sdkSrcDir, c.outPath).replace(/\.ts$/, ".js");
3799
4280
  if (!rel.startsWith(".")) rel = "./" + rel;
3800
4281
  rootExports.push(`export * from '${rel}';`);
3801
4282
  }
3802
4283
  for (const barrel of sdkTypeBarrels) {
3803
- let rel = relative6(sdkSrcDir, barrel.outPath).replace(/\.ts$/, ".js");
4284
+ let rel = relative7(sdkSrcDir, barrel.outPath).replace(/\.ts$/, ".js");
3804
4285
  if (!rel.startsWith(".")) rel = "./" + rel;
3805
4286
  rootExports.push(`export * from '${rel}';`);
3806
4287
  }
@@ -3936,7 +4417,8 @@ function collectTypesOutput(config, rootDir, inputs, units) {
3936
4417
  modelOutPaths,
3937
4418
  currentOutPath: outPath,
3938
4419
  modelsWithInput,
3939
- modelsWithOutput
4420
+ modelsWithOutput,
4421
+ target: config.target
3940
4422
  })
3941
4423
  }
3942
4424
  ], "render")
@@ -3944,6 +4426,114 @@ function collectTypesOutput(config, rootDir, inputs, units) {
3944
4426
  }
3945
4427
  }
3946
4428
  __name(collectTypesOutput, "collectTypesOutput");
4429
+ function resolveMcpModelOutPaths(config, rootDir, contractRoots, commonRoot, modelsWithInput, modelsWithOutput) {
4430
+ const map = /* @__PURE__ */ new Map();
4431
+ let base;
4432
+ let template;
4433
+ let suffix;
4434
+ if (config.mcp?.output?.types) {
4435
+ base = resolve2(rootDir, config.mcp.baseDir ?? ".");
4436
+ template = config.mcp.output.types;
4437
+ suffix = ".ts";
4438
+ } else if (config.server?.zod && config.server.output?.types) {
4439
+ base = resolve2(rootDir, config.server.baseDir ?? ".");
4440
+ template = config.server.output.types;
4441
+ suffix = ".ts";
4442
+ } else if (config.zod) {
4443
+ base = resolve2(rootDir, config.zod.baseDir ?? ".");
4444
+ template = config.zod.output;
4445
+ suffix = ".schema.ts";
4446
+ } else {
4447
+ return map;
4448
+ }
4449
+ for (const ast of contractRoots) {
4450
+ const outPath = computeContractOutPath(ast.file, base, template, suffix, commonRoot, ast.meta);
4451
+ for (const model of ast.models) {
4452
+ map.set(model.name, outPath);
4453
+ if (modelsWithInput.has(model.name)) map.set(`${model.name}Input`, outPath);
4454
+ if (modelsWithOutput.has(model.name)) map.set(`${model.name}Output`, outPath);
4455
+ }
4456
+ }
4457
+ return map;
4458
+ }
4459
+ __name(resolveMcpModelOutPaths, "resolveMcpModelOutPaths");
4460
+ function collectMcpOutput(config, fullConfig, rootDir, inputs, units, globalFiles) {
4461
+ const mcpBase = resolve2(rootDir, config.baseDir ?? ".");
4462
+ const modelsWithInput = inputs.modelsWithInput;
4463
+ const modelsWithOutput = inputs.modelsWithOutput;
4464
+ const modelMap = buildModelMap(inputs.contractRoots);
4465
+ const allFiles = [
4466
+ ...inputs.contractRoots.map((r) => r.file),
4467
+ ...inputs.opRoots.map((r) => r.file)
4468
+ ];
4469
+ const commonRoot = commonDir(allFiles, rootDir);
4470
+ const subConfigKey = stableSubConfig(config);
4471
+ const includeInternal = config.includeInternal ?? false;
4472
+ const modelOutPaths = resolveMcpModelOutPaths(fullConfig, rootDir, inputs.contractRoots, commonRoot, modelsWithInput, modelsWithOutput);
4473
+ const entries = [];
4474
+ for (const ast of inputs.opRoots) {
4475
+ if (!hasMcpOperations(ast, includeInternal)) continue;
4476
+ const outPath = computeOpOutPath(ast.file, mcpBase, config.output?.tools, ".mcp.ts", commonRoot, ast.meta);
4477
+ const refs = collectOpRootRefs(ast, modelMap);
4478
+ const fingerprint = hashFingerprint({
4479
+ kind: "mcp-tools",
4480
+ v: TYPESCRIPT_CODEGEN_VERSION,
4481
+ outPath,
4482
+ root: ast,
4483
+ outPathSlice: sliceOutPathMap(refs, modelOutPaths, modelsWithInput, modelsWithOutput),
4484
+ modelsWithInput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithInput),
4485
+ modelsWithOutput: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithOutput),
4486
+ servicePathTemplate: config.servicePathTemplate ?? null,
4487
+ includeInternal,
4488
+ sub: subConfigKey
4489
+ });
4490
+ units.push({
4491
+ key: `mcp-tools::${outPath}`,
4492
+ fingerprint,
4493
+ render: /* @__PURE__ */ __name(() => [
4494
+ {
4495
+ relativePath: outPath,
4496
+ content: generateMcpFile(ast, {
4497
+ outPath,
4498
+ modelOutPaths,
4499
+ modelsWithInput,
4500
+ modelsWithOutput,
4501
+ servicePathTemplate: config.servicePathTemplate,
4502
+ includeInternal
4503
+ })
4504
+ }
4505
+ ], "render")
4506
+ });
4507
+ entries.push({
4508
+ outPath,
4509
+ registerFn: deriveMcpRegisterFnName(ast.file)
4510
+ });
4511
+ }
4512
+ if (entries.length === 0) return;
4513
+ const indexPath = join2(mcpBase, config.output?.index ?? "mcp.tools.ts");
4514
+ const aggregatorEntries = entries.map((e) => {
4515
+ let rel = relative7(dirname7(indexPath), e.outPath).replace(/\.ts$/, ".js");
4516
+ if (!rel.startsWith(".")) rel = "./" + rel;
4517
+ return {
4518
+ registerFn: e.registerFn,
4519
+ importPath: rel
4520
+ };
4521
+ }).sort((a, b) => a.registerFn.localeCompare(b.registerFn));
4522
+ globalFiles.push({
4523
+ relativePath: indexPath,
4524
+ content: generateMcpAggregator(aggregatorEntries)
4525
+ });
4526
+ if (config.emitRouter !== false) {
4527
+ const routerPath = join2(mcpBase, config.output?.router ?? "mcp.router.ts");
4528
+ globalFiles.push({
4529
+ relativePath: routerPath,
4530
+ content: generateMcpRouter({
4531
+ path: config.path
4532
+ })
4533
+ });
4534
+ }
4535
+ }
4536
+ __name(collectMcpOutput, "collectMcpOutput");
3947
4537
  function readManifest(manifestPath) {
3948
4538
  if (!existsSync(manifestPath)) return emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION);
3949
4539
  try {
@@ -3955,7 +4545,7 @@ function readManifest(manifestPath) {
3955
4545
  __name(readManifest, "readManifest");
3956
4546
  function writeManifest(manifestPath, manifest) {
3957
4547
  try {
3958
- mkdirSync(dirname6(manifestPath), {
4548
+ mkdirSync(dirname7(manifestPath), {
3959
4549
  recursive: true
3960
4550
  });
3961
4551
  writeFileSync(manifestPath, serializeIncrementalManifest(manifest), "utf-8");
@@ -3971,7 +4561,7 @@ function deleteStalePaths(absPaths) {
3971
4561
  rmSync(abs, {
3972
4562
  force: true
3973
4563
  });
3974
- removedDirs.add(dirname6(abs));
4564
+ removedDirs.add(dirname7(abs));
3975
4565
  }
3976
4566
  }
3977
4567
  for (const dir of removedDirs) {
@@ -3980,7 +4570,7 @@ function deleteStalePaths(absPaths) {
3980
4570
  try {
3981
4571
  if (readdirSync(current).length === 0) {
3982
4572
  rmdirSync(current);
3983
- current = dirname6(current);
4573
+ current = dirname7(current);
3984
4574
  } else {
3985
4575
  break;
3986
4576
  }