@contractkit/plugin-typescript 0.28.1 → 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.
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
@@ -1290,7 +1290,7 @@ function generateHandler(route, op, root, options) {
1290
1290
  middlewares.push(`requireSignature(${sigArgs})`);
1291
1291
  }
1292
1292
  const middlewareStr = middlewares.length > 0 ? `, ${middlewares.join(", ")},` : ",";
1293
- lines.push(`${deriveRouterName(file)}.${method}('${path}'${middlewareStr} async (ctx, next) => {`);
1293
+ lines.push(`${deriveRouterName(file)}.${method}('${path}'${middlewareStr} async ctx => {`);
1294
1294
  lines.push(...generateParamValidation(route.params, "ctx.params", "params", route.paramsMode ?? "strict", "", modelsWithInput));
1295
1295
  lines.push(...generateParamValidation(op.query, "ctx.query", "query", op.queryMode ?? "strict", "", modelsWithInput));
1296
1296
  lines.push(...generateParamValidation(op.headers, "ctx.headers", "headers", op.headersMode ?? "strip", "", modelsWithInput));
@@ -1321,13 +1321,13 @@ function generateHandler(route, op, root, options) {
1321
1321
  lines.push("");
1322
1322
  }
1323
1323
  }
1324
- const primaryResponse = op.responses.find((r) => r.bodyType) ?? op.responses[0];
1324
+ const primaryResponse2 = op.responses.find((r) => r.bodyType) ?? op.responses[0];
1325
1325
  const serviceParts = inferService(op, route, file);
1326
- const respHeaders = primaryResponse?.headers ?? [];
1326
+ const respHeaders = primaryResponse2?.headers ?? [];
1327
1327
  const hasRespHeaders = respHeaders.length > 0;
1328
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);
1329
+ if (primaryResponse2?.bodyType) {
1330
+ const { annotation, prelude } = formatTypeAnnotation(primaryResponse2.bodyType, options.modelsWithOutput);
1331
1331
  if (prelude) {
1332
1332
  lines.push(` ${prelude}`);
1333
1333
  }
@@ -1346,7 +1346,7 @@ function generateHandler(route, op, root, options) {
1346
1346
  }
1347
1347
  }
1348
1348
  lines.push("");
1349
- lines.push(` ctx.status = ${primaryResponse?.statusCode ?? 200};`);
1349
+ lines.push(` ctx.status = ${primaryResponse2?.statusCode ?? 200};`);
1350
1350
  if (hasRespHeaders) {
1351
1351
  for (const h of respHeaders) {
1352
1352
  const accessor = `result.headers[${JSON.stringify(headerNameToProperty(h.name))}]`;
@@ -1357,8 +1357,8 @@ function generateHandler(route, op, root, options) {
1357
1357
  }
1358
1358
  }
1359
1359
  }
1360
- if (primaryResponse?.bodyType && primaryResponse.contentType) {
1361
- lines.push(` ctx.type = '${primaryResponse.contentType}';`);
1360
+ if (primaryResponse2?.bodyType && primaryResponse2.contentType) {
1361
+ lines.push(` ctx.type = '${primaryResponse2.contentType}';`);
1362
1362
  lines.push(` ctx.body = ${hasRespHeaders ? "result.body" : "result"};`);
1363
1363
  }
1364
1364
  lines.push(`});`);
@@ -1960,11 +1960,11 @@ function generateMethod(route, op, file, options) {
1960
1960
  const { modelsWithInput, modelsWithOutput } = options;
1961
1961
  const params = buildMethodParams(route, op, modelsWithInput);
1962
1962
  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 ?? [];
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 ?? [];
1968
1968
  const hasRespHeaders = respHeaders.length > 0;
1969
1969
  const headersShape = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`).join("; ")} }` : "";
1970
1970
  const returnType = hasRespHeaders ? isVoid ? `{ headers: ${headersShape} }` : `{ data: ${dataType}; headers: ${headersShape} }` : dataType;
@@ -3039,12 +3039,443 @@ function renderOutputField(field, outputCase, modelsWithOutput) {
3039
3039
  }
3040
3040
  __name(renderOutputField, "renderOutputField");
3041
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
+
3042
3473
  // src/path-utils.ts
3043
- import { resolve, join, relative as relative5, dirname as dirname5, isAbsolute } from "path";
3474
+ import { resolve, join, relative as relative6, dirname as dirname6, isAbsolute } from "path";
3044
3475
  import { collectTypeRefs as collectTypeRefs2, collectPublicTypeNames } from "@contractkit/core";
3045
3476
  var TEMPLATE_VAR_RE = /\{\w+\}/;
3046
3477
  function assertWithinBase(baseOutDir, outPath) {
3047
- const rel = relative5(resolve(baseOutDir), resolve(outPath));
3478
+ const rel = relative6(resolve(baseOutDir), resolve(outPath));
3048
3479
  if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
3049
3480
  throw new Error(`Refusing to emit outside output directory: resolved path "${outPath}" escapes "${baseOutDir}" (check options { keys } values used in output path templates)`);
3050
3481
  }
@@ -3062,7 +3493,7 @@ function includesFilename(p) {
3062
3493
  __name(includesFilename, "includesFilename");
3063
3494
  function commonDir(files, rootDir) {
3064
3495
  if (files.length === 0) return resolve(rootDir);
3065
- const parts = files.map((f) => dirname5(f).split("/"));
3496
+ const parts = files.map((f) => dirname6(f).split("/"));
3066
3497
  const first = parts[0];
3067
3498
  let depth = first.length;
3068
3499
  for (const p of parts) {
@@ -3078,7 +3509,7 @@ function commonDir(files, rootDir) {
3078
3509
  __name(commonDir, "commonDir");
3079
3510
  function computeOpOutPath(filePath, baseDir, output, defaultSuffix, commonRoot, meta = {}) {
3080
3511
  const baseName = filePath.split("/").pop();
3081
- const relDir = relative5(commonRoot, dirname5(filePath));
3512
+ const relDir = relative6(commonRoot, dirname6(filePath));
3082
3513
  const filename = baseName.replace(/\.ck$/, "");
3083
3514
  const defaultName = `${filename}${defaultSuffix}`;
3084
3515
  const baseOutDir = resolve(baseDir);
@@ -3108,7 +3539,7 @@ function computeSdkOutPath(filePath, rootDir, clientOutput, commonRoot, meta = {
3108
3539
  const baseName = filePath.split("/").pop();
3109
3540
  const defaultOutName = baseName.replace(/\.ck$/, ".client.ts");
3110
3541
  const baseOutDir = resolve(rootDir);
3111
- const relDir = relative5(commonRoot, dirname5(filePath));
3542
+ const relDir = relative6(commonRoot, dirname6(filePath));
3112
3543
  const filename = baseName.replace(/\.ck$/, "");
3113
3544
  if (clientOutput && TEMPLATE_VAR_RE.test(clientOutput)) {
3114
3545
  const resolved = resolveTemplate(clientOutput, {
@@ -3160,7 +3591,7 @@ function computeSdkTypeOutPath(filePath, rootDir, typeOutput, commonRoot, meta =
3160
3591
  const baseName = filePath.split("/").pop();
3161
3592
  const defaultOutName = baseName.replace(/\.ck$/, ".ts");
3162
3593
  const baseOutDir = resolve(rootDir);
3163
- const relDir = relative5(commonRoot, dirname5(filePath));
3594
+ const relDir = relative6(commonRoot, dirname6(filePath));
3164
3595
  const filename = baseName.replace(/\.ck$/, "");
3165
3596
  if (TEMPLATE_VAR_RE.test(typeOutput)) {
3166
3597
  const resolved = resolveTemplate(typeOutput, {
@@ -3179,7 +3610,7 @@ __name(computeSdkTypeOutPath, "computeSdkTypeOutPath");
3179
3610
  function generateBarrelFiles(contractPaths) {
3180
3611
  const byDir = /* @__PURE__ */ new Map();
3181
3612
  for (const outPath of contractPaths) {
3182
- const dir = dirname5(outPath);
3613
+ const dir = dirname6(outPath);
3183
3614
  const group = byDir.get(dir) ?? [];
3184
3615
  group.push(outPath);
3185
3616
  byDir.set(dir, group);
@@ -3273,6 +3704,7 @@ async function runTypescriptCodegen(inputs, ctx, config, rootDir) {
3273
3704
  if (config.sdk) collectSdkOutput(config.sdk, rootDir, inputs, units, globalFiles);
3274
3705
  if (config.zod) collectZodOutput(config.zod, rootDir, inputs, units);
3275
3706
  if (config.types) collectTypesOutput(config.types, rootDir, inputs, units);
3707
+ if (config.mcp) collectMcpOutput(config.mcp, config, rootDir, inputs, units, globalFiles);
3276
3708
  const result = runIncrementalCodegen({
3277
3709
  codegenVersion: TYPESCRIPT_CODEGEN_VERSION,
3278
3710
  prevManifest,
@@ -3479,7 +3911,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3479
3911
  const sdkEntryPath = sdkOutput ? join2(sdkBase, TEMPLATE_VAR_RE.test(sdkOutput) ? resolveTemplate(sdkOutput, {
3480
3912
  name: sdkName ?? "sdk"
3481
3913
  }) : sdkOutput) : join2(sdkBase, "sdk.ts");
3482
- const sdkOptionsPath = join2(dirname6(sdkEntryPath), "sdk-options.ts");
3914
+ const sdkOptionsPath = join2(dirname7(sdkEntryPath), "sdk-options.ts");
3483
3915
  const subConfigKey = stableSubConfig(config);
3484
3916
  const modelsWithInput = inputs.modelsWithInput;
3485
3917
  const modelsWithOutput = inputs.modelsWithOutput;
@@ -3538,7 +3970,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3538
3970
  modelsWithOutput
3539
3971
  });
3540
3972
  } else {
3541
- let rel = relative6(dirname6(typeOutPath), sdkOptionsPath).replace(/\.ts$/, ".js");
3973
+ let rel = relative7(dirname7(typeOutPath), sdkOptionsPath).replace(/\.ts$/, ".js");
3542
3974
  if (!rel.startsWith(".")) rel = "./" + rel;
3543
3975
  content = generatePlainTypes(ast, {
3544
3976
  modelOutPaths: sdkModelOutPaths,
@@ -3679,12 +4111,12 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3679
4111
  const hasAnything = sdkClientInfos.length > 0 || areaBuckets.size > 0;
3680
4112
  const areaClientOutPaths = /* @__PURE__ */ new Map();
3681
4113
  if (hasAnything) {
3682
- const sdkEntryDir = dirname6(sdkEntryPath);
3683
- const sdkOptionsRel = relative6(sdkEntryDir, sdkOptionsPath).replace(/\.ts$/, ".js");
4114
+ const sdkEntryDir = dirname7(sdkEntryPath);
4115
+ const sdkOptionsRel = relative7(sdkEntryDir, sdkOptionsPath).replace(/\.ts$/, ".js");
3684
4116
  const sdkOptionsImportPath = sdkOptionsRel.startsWith(".") ? sdkOptionsRel : "./" + sdkOptionsRel;
3685
4117
  const sdkClassName = sdkName ? sdkName.split(/[-._\s]+/).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Sdk" : "Sdk";
3686
4118
  const toClientImport = /* @__PURE__ */ __name((sourceDir, info) => {
3687
- let rel = relative6(sourceDir, info.outPath).replace(/\.ts$/, ".js");
4119
+ let rel = relative7(sourceDir, info.outPath).replace(/\.ts$/, ".js");
3688
4120
  if (!rel.startsWith(".")) rel = "./" + rel;
3689
4121
  return {
3690
4122
  className: info.className,
@@ -3713,7 +4145,7 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3713
4145
  });
3714
4146
  const subareaClients = bucket.leaves.sort((a, b) => a.subarea.localeCompare(b.subarea)).map((l) => ({
3715
4147
  propertyName: deriveSubareaPropertyName(l.subarea),
3716
- client: toClientImport(dirname6(areaClientOutPath), {
4148
+ client: toClientImport(dirname7(areaClientOutPath), {
3717
4149
  outPath: l.outPath,
3718
4150
  className: deriveSubareaClientClassName(area, l.subarea),
3719
4151
  propertyName: deriveSubareaPropertyName(l.subarea)
@@ -3784,23 +4216,23 @@ function collectSdkOutput(config, rootDir, inputs, units, globalFiles) {
3784
4216
  })
3785
4217
  });
3786
4218
  }
3787
- const sdkSrcDir = dirname6(sdkEntryPath);
4219
+ const sdkSrcDir = dirname7(sdkEntryPath);
3788
4220
  const sdkTypeBarrels = generateBarrelFiles(sdkTypePaths);
3789
4221
  for (const barrel of sdkTypeBarrels) globalFiles.push({
3790
4222
  relativePath: barrel.outPath,
3791
4223
  content: barrel.content
3792
4224
  });
3793
4225
  const rootExports = [
3794
- `export * from './${basename3(sdkOptionsPath).replace(/\.ts$/, ".js")}';`
4226
+ `export * from './${basename4(sdkOptionsPath).replace(/\.ts$/, ".js")}';`
3795
4227
  ];
3796
- if (hasAnything) rootExports.push(`export * from './${basename3(sdkEntryPath).replace(/\.ts$/, ".js")}';`);
4228
+ if (hasAnything) rootExports.push(`export * from './${basename4(sdkEntryPath).replace(/\.ts$/, ".js")}';`);
3797
4229
  for (const c of sdkClientInfos) {
3798
- let rel = relative6(sdkSrcDir, c.outPath).replace(/\.ts$/, ".js");
4230
+ let rel = relative7(sdkSrcDir, c.outPath).replace(/\.ts$/, ".js");
3799
4231
  if (!rel.startsWith(".")) rel = "./" + rel;
3800
4232
  rootExports.push(`export * from '${rel}';`);
3801
4233
  }
3802
4234
  for (const barrel of sdkTypeBarrels) {
3803
- let rel = relative6(sdkSrcDir, barrel.outPath).replace(/\.ts$/, ".js");
4235
+ let rel = relative7(sdkSrcDir, barrel.outPath).replace(/\.ts$/, ".js");
3804
4236
  if (!rel.startsWith(".")) rel = "./" + rel;
3805
4237
  rootExports.push(`export * from '${rel}';`);
3806
4238
  }
@@ -3944,6 +4376,114 @@ function collectTypesOutput(config, rootDir, inputs, units) {
3944
4376
  }
3945
4377
  }
3946
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");
3947
4487
  function readManifest(manifestPath) {
3948
4488
  if (!existsSync(manifestPath)) return emptyIncrementalManifest(TYPESCRIPT_CODEGEN_VERSION);
3949
4489
  try {
@@ -3955,7 +4495,7 @@ function readManifest(manifestPath) {
3955
4495
  __name(readManifest, "readManifest");
3956
4496
  function writeManifest(manifestPath, manifest) {
3957
4497
  try {
3958
- mkdirSync(dirname6(manifestPath), {
4498
+ mkdirSync(dirname7(manifestPath), {
3959
4499
  recursive: true
3960
4500
  });
3961
4501
  writeFileSync(manifestPath, serializeIncrementalManifest(manifest), "utf-8");
@@ -3971,7 +4511,7 @@ function deleteStalePaths(absPaths) {
3971
4511
  rmSync(abs, {
3972
4512
  force: true
3973
4513
  });
3974
- removedDirs.add(dirname6(abs));
4514
+ removedDirs.add(dirname7(abs));
3975
4515
  }
3976
4516
  }
3977
4517
  for (const dir of removedDirs) {
@@ -3980,7 +4520,7 @@ function deleteStalePaths(absPaths) {
3980
4520
  try {
3981
4521
  if (readdirSync(current).length === 0) {
3982
4522
  rmdirSync(current);
3983
- current = dirname6(current);
4523
+ current = dirname7(current);
3984
4524
  } else {
3985
4525
  break;
3986
4526
  }