@kubb/plugin-mcp 5.0.0-beta.85 → 5.0.0-beta.87

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
@@ -1,9 +1,9 @@
1
1
  import "./rolldown-runtime-C0LytTxp.js";
2
- import path from "node:path";
3
- import { ast, defineGenerator, definePlugin, defineResolver } from "kubb/kit";
2
+ import { Resolver, ast, createResolver, defineGenerator, definePlugin } from "kubb/kit";
4
3
  import { Const, File, Function, jsxRenderer } from "kubb/jsx";
5
4
  import { Fragment, jsx, jsxs } from "kubb/jsx/jsx-runtime";
6
5
  import { createFunctionParameter, createFunctionParameters, createObjectBindingPattern, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
6
+ import path from "node:path";
7
7
  import { pluginZodName } from "@kubb/plugin-zod";
8
8
  //#region ../../internals/utils/src/casing.ts
9
9
  /**
@@ -32,31 +32,6 @@ function camelCase(text, { prefix = "", suffix = "" } = {}) {
32
32
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
33
33
  }
34
34
  //#endregion
35
- //#region ../../internals/utils/src/fs.ts
36
- /**
37
- * Builds a nested file path from a dotted name. Splits on dots that precede a letter
38
- * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
39
- * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
40
- *
41
- * Empty segments are dropped before joining. They arise when the name starts with a dot
42
- * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
43
- * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
44
- * absolute path, letting generated files escape the configured output directory.
45
- *
46
- * @example Nested path from a dotted name
47
- * `toFilePath('pet.petId') // 'pet/petId'`
48
- *
49
- * @example PascalCase the final segment
50
- * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
51
- *
52
- * @example Suffix applied to the final segment only
53
- * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
54
- */
55
- function toFilePath(name, caseLast = camelCase) {
56
- const parts = name.split(/\.(?=[a-zA-Z])/);
57
- return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
58
- }
59
- //#endregion
60
35
  //#region ../../internals/utils/src/reserved.ts
61
36
  /**
62
37
  * JavaScript and Java reserved words.
@@ -157,6 +132,22 @@ const reservedWords = /* @__PURE__ */ new Set([
157
132
  */
158
133
  function isValidVarName(name) {
159
134
  if (!name || reservedWords.has(name)) return false;
135
+ return isIdentifier(name);
136
+ }
137
+ /**
138
+ * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
139
+ *
140
+ * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
141
+ * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
142
+ * deciding whether an object key needs quoting.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * isIdentifier('name') // true
147
+ * isIdentifier('x-total')// false
148
+ * ```
149
+ */
150
+ function isIdentifier(name) {
160
151
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
161
152
  }
162
153
  //#endregion
@@ -312,13 +303,13 @@ function dedupeByCasedName(params) {
312
303
  //#region ../../internals/shared/src/operation.ts
313
304
  /**
314
305
  * Builds the `ResolverFileParams` every operation generator passes to
315
- * `resolver.resolveFile`: a file named `name`, tagged by the operation's first
306
+ * `resolver.file`: a file named `name`, tagged by the operation's first
316
307
  * tag (or `'default'`), at the operation's path. Centralizes the entry object
317
308
  * that was repeated at dozens of call sites across the client and query plugins.
318
309
  *
319
310
  * @example
320
311
  * ```ts
321
- * resolver.resolveFile(operationFileEntry(node, node.operationId), { root, output, group })
312
+ * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
322
313
  * ```
323
314
  */
324
315
  function operationFileEntry(node, name, extname = ".ts") {
@@ -335,8 +326,11 @@ function getOperationLink(node, link) {
335
326
  if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
336
327
  return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
337
328
  }
338
- function getContentTypeInfo(node) {
339
- const contentTypes = node.requestBody?.content?.map((e) => e.contentType) ?? [];
329
+ /**
330
+ * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
331
+ * are present and the union, default, and form-data flags the client uses to pick one.
332
+ */
333
+ function buildContentTypeInfo(contentTypes) {
340
334
  const isMultipleContentTypes = contentTypes.length > 1;
341
335
  return {
342
336
  contentTypes,
@@ -346,20 +340,15 @@ function getContentTypeInfo(node) {
346
340
  hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
347
341
  };
348
342
  }
343
+ function getContentTypeInfo(node) {
344
+ return buildContentTypeInfo(node.requestBody?.content?.map((e) => e.contentType) ?? []);
345
+ }
349
346
  /**
350
347
  * The request-body counterpart for the primary success response: the content types it documents and
351
348
  * whether several are present, so the client can let a caller pick which one to accept.
352
349
  */
353
350
  function getResponseContentTypeInfo(node) {
354
- const contentTypes = getPrimarySuccessResponse(node)?.content?.map((e) => e.contentType) ?? [];
355
- const isMultipleContentTypes = contentTypes.length > 1;
356
- return {
357
- contentTypes,
358
- isMultipleContentTypes,
359
- contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
360
- defaultContentType: contentTypes[0] ?? "application/json",
361
- hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
362
- };
351
+ return buildContentTypeInfo(getPrimarySuccessResponse(node)?.content?.map((e) => e.contentType) ?? []);
363
352
  }
364
353
  function buildRequestConfigType(node) {
365
354
  const request = getContentTypeInfo(node);
@@ -401,7 +390,7 @@ function getRequestGroupOptionality(node) {
401
390
  }
402
391
  /**
403
392
  * Builds the grouped `{ path, query, body, headers }` parameter for a generated client
404
- * function, typed from the operation's `RequestConfig` (minus `url`). Only the groups the
393
+ * function, typed from the operation's `Options` (minus `url`). Only the groups the
405
394
  * operation actually has are destructured. The trailing `config` parameter carries the
406
395
  * runtime `RequestConfig` overrides plus `client`.
407
396
  */
@@ -415,7 +404,7 @@ function buildRequestParamsSignature(node, resolver, options = {}) {
415
404
  "headers"
416
405
  ].filter((key) => groups[key]);
417
406
  return {
418
- signature: [names.length > 0 ? `{ ${names.join(", ")} }: ${resolver.resolveRequestConfigName(node)}${isOptional ? " = {}" : ""}` : null, isConfigurable ? `config: ${buildRequestConfigType(node)} = {}` : null].filter(Boolean).join(", "),
407
+ signature: [names.length > 0 ? `{ ${names.join(", ")} }: ${resolver.response.options(node)}${isOptional ? " = {}" : ""}` : null, isConfigurable ? `config: ${buildRequestConfigType(node)} = {}` : null].filter(Boolean).join(", "),
419
408
  groups
420
409
  };
421
410
  }
@@ -744,15 +733,15 @@ function resolveClientOperation(options) {
744
733
  const { clientPlugin, driver, node, root, output } = options;
745
734
  if (!clientPlugin) return null;
746
735
  const resolver = driver.getResolver(clientPlugin.pluginName);
747
- if (!resolver) return null;
748
736
  const plugin = driver.getPlugin(clientPlugin.pluginName);
749
- const file = resolver.resolveFile(operationFileEntry(node, node.operationId), {
737
+ const file = resolver.file({
738
+ ...operationFileEntry(node, node.operationId),
750
739
  root,
751
740
  output: plugin?.options?.output ?? output,
752
741
  group: plugin?.options?.group ?? void 0
753
742
  });
754
743
  return {
755
- name: resolver.resolveName(node.operationId),
744
+ name: resolver.name(node.operationId),
756
745
  path: file.path,
757
746
  clientPath: path.resolve(root, ".kubb/client.ts")
758
747
  };
@@ -783,25 +772,23 @@ const mcpGenerator = defineGenerator({
783
772
  output
784
773
  });
785
774
  if (!contractOp) return null;
786
- const requestConfigName = tsResolver.resolveRequestConfigName(node);
775
+ const optionsName = tsResolver.response.options(node);
787
776
  const meta = {
788
- name: resolver.resolveHandlerName(node),
789
- file: resolver.resolveFile({
777
+ name: resolver.handler.name(node),
778
+ file: resolver.file({
790
779
  name: node.operationId,
791
780
  extname: ".ts",
792
781
  tag: node.tags[0] ?? "default",
793
- path: node.path
794
- }, {
782
+ path: node.path,
795
783
  root,
796
784
  output,
797
785
  group: group ?? void 0
798
786
  }),
799
- fileTs: tsResolver.resolveFile({
787
+ fileTs: tsResolver.file({
800
788
  name: node.operationId,
801
789
  extname: ".ts",
802
790
  tag: node.tags[0] ?? "default",
803
- path: node.path
804
- }, {
791
+ path: node.path,
805
792
  root,
806
793
  output: pluginTs.options?.output ?? output,
807
794
  group: pluginTs.options?.group ?? void 0
@@ -812,8 +799,8 @@ const mcpGenerator = defineGenerator({
812
799
  path: meta.file.path,
813
800
  meta: meta.file.meta,
814
801
  children: [
815
- meta.fileTs && requestConfigName && /* @__PURE__ */ jsx(File.Import, {
816
- name: [requestConfigName],
802
+ meta.fileTs && optionsName && /* @__PURE__ */ jsx(File.Import, {
803
+ name: [optionsName],
817
804
  root: meta.file.path,
818
805
  path: meta.fileTs.path,
819
806
  isTypeOnly: true
@@ -878,32 +865,30 @@ const serverGenerator = defineGenerator({
878
865
  };
879
866
  const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
880
867
  const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
881
- const mcpFile = resolver.resolveFile({
868
+ const mcpFile = resolver.file({
882
869
  name: node.operationId,
883
870
  extname: ".ts",
884
871
  tag: node.tags[0] ?? "default",
885
- path: node.path
886
- }, {
872
+ path: node.path,
887
873
  root,
888
874
  output,
889
875
  group: group ?? void 0
890
876
  });
891
- const zodFile = zodResolver.resolveFile({
877
+ const zodFile = zodResolver.file({
892
878
  name: node.operationId,
893
879
  extname: ".ts",
894
880
  tag: node.tags[0] ?? "default",
895
- path: node.path
896
- }, {
881
+ path: node.path,
897
882
  root,
898
883
  output: pluginZod.options?.output ?? output,
899
884
  group: pluginZod.options?.group ?? void 0
900
885
  });
901
- const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null;
886
+ const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.response.body(node) : null;
902
887
  const successStatus = findSuccessStatusCode(node.responses);
903
- const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null;
888
+ const responseName = successStatus ? zodResolver.response.status(node, successStatus) : null;
904
889
  const resolveParams = (params) => params.map((p) => ({
905
890
  name: p.name,
906
- schemaName: zodResolver.resolveParamName(node, p)
891
+ schemaName: zodResolver.param.name(node, p)
907
892
  }));
908
893
  return {
909
894
  tool: {
@@ -912,7 +897,7 @@ const serverGenerator = defineGenerator({
912
897
  description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`
913
898
  },
914
899
  mcp: {
915
- name: resolver.resolveHandlerName(node),
900
+ name: resolver.handler.name(node),
916
901
  file: mcpFile
917
902
  },
918
903
  zod: {
@@ -949,7 +934,7 @@ const serverGenerator = defineGenerator({
949
934
  baseName: serverFile.baseName,
950
935
  path: serverFile.path,
951
936
  meta: serverFile.meta,
952
- banner: resolver.resolveBanner(ctx.meta, {
937
+ banner: resolver.default.banner(ctx.meta, {
953
938
  output,
954
939
  config,
955
940
  file: {
@@ -957,7 +942,7 @@ const serverGenerator = defineGenerator({
957
942
  baseName: serverFile.baseName
958
943
  }
959
944
  }),
960
- footer: resolver.resolveFooter(ctx.meta, {
945
+ footer: resolver.default.footer(ctx.meta, {
961
946
  output,
962
947
  config,
963
948
  file: {
@@ -1018,26 +1003,18 @@ const serverGenerator = defineGenerator({
1018
1003
  * ```ts
1019
1004
  * import { resolverMcp } from '@kubb/plugin-mcp'
1020
1005
  *
1021
- * resolverMcp.default('addPet', 'function') // 'addPetHandler'
1006
+ * resolverMcp.name('addPet') // 'addPetHandler'
1022
1007
  * ```
1023
1008
  */
1024
- const resolverMcp = defineResolver(() => ({
1025
- name: "default",
1009
+ const resolverMcp = createResolver({
1026
1010
  pluginName: "plugin-mcp",
1027
- default(name, type) {
1028
- if (type === "file") return toFilePath(name);
1011
+ name(name) {
1029
1012
  return camelCase(name, { suffix: "handler" });
1030
1013
  },
1031
- resolveName(name) {
1032
- return this.default(name, "function");
1033
- },
1034
- resolvePathName(name, type) {
1035
- return this.default(name, type);
1036
- },
1037
- resolveHandlerName(node) {
1038
- return this.resolveName(node.operationId);
1039
- }
1040
- }));
1014
+ handler: { name(node) {
1015
+ return this.name(node.operationId);
1016
+ } }
1017
+ });
1041
1018
  //#endregion
1042
1019
  //#region src/plugin.ts
1043
1020
  /**
@@ -1083,10 +1060,7 @@ const pluginMcp = definePlugin((options) => {
1083
1060
  options,
1084
1061
  dependencies: [pluginTsName, pluginZodName],
1085
1062
  hooks: { "kubb:plugin:setup"(ctx) {
1086
- const resolver = userResolver ? {
1087
- ...resolverMcp,
1088
- ...userResolver
1089
- } : resolverMcp;
1063
+ const resolver = userResolver ? Resolver.merge(resolverMcp, userResolver) : resolverMcp;
1090
1064
  const resolvedClient = resolveClient({
1091
1065
  client,
1092
1066
  pluginNames: (ctx.config.plugins ?? []).map((p) => p.name).filter((name) => Boolean(name))