@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/.turbo/turbo-build$colon$ci.log +5 -5
- package/.turbo/turbo-test$colon$ci.log +17 -15
- package/CHANGELOG.md +6 -0
- package/dist/codegen-mcp.d.ts +38 -0
- package/dist/codegen-mcp.d.ts.map +1 -0
- package/dist/codegen-operation.d.ts +42 -1
- package/dist/codegen-operation.d.ts.map +1 -1
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +575 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen-mcp.ts +501 -0
- package/src/codegen-operation.ts +39 -5
- package/src/index.ts +154 -0
- package/tests/codegen-mcp.test.ts +246 -0
- package/tests/codegen-operation.test.ts +18 -0
- package/tests/pipeline.test.ts +59 -0
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
|
|
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
|
|
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
|
|
1324
|
+
const primaryResponse2 = op.responses.find((r) => r.bodyType) ?? op.responses[0];
|
|
1325
1325
|
const serviceParts = inferService(op, route, file);
|
|
1326
|
-
const respHeaders =
|
|
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 (
|
|
1330
|
-
const { annotation, prelude } = formatTypeAnnotation(
|
|
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 = ${
|
|
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 (
|
|
1361
|
-
lines.push(` ctx.type = '${
|
|
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
|
|
1964
|
-
const isVoid = !
|
|
1965
|
-
const respCategory =
|
|
1966
|
-
const dataType = isVoid ? "void" : respCategory === "text" ? "string" : respCategory === "binary" ? "Blob" : renderOutputTsType(
|
|
1967
|
-
const respHeaders =
|
|
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
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
3683
|
-
const sdkOptionsRel =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 './${
|
|
4226
|
+
`export * from './${basename4(sdkOptionsPath).replace(/\.ts$/, ".js")}';`
|
|
3795
4227
|
];
|
|
3796
|
-
if (hasAnything) rootExports.push(`export * from './${
|
|
4228
|
+
if (hasAnything) rootExports.push(`export * from './${basename4(sdkEntryPath).replace(/\.ts$/, ".js")}';`);
|
|
3797
4229
|
for (const c of sdkClientInfos) {
|
|
3798
|
-
let rel =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
4523
|
+
current = dirname7(current);
|
|
3984
4524
|
} else {
|
|
3985
4525
|
break;
|
|
3986
4526
|
}
|