@kubb/plugin-mcp 5.2.0 → 5.2.1

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,28 +1,10 @@
1
1
  import "./rolldown-runtime-C0LytTxp.js";
2
- import { Resolver, Url, addGenerator, ast, createResolver, defineExtension, defineGenerator, setMacros, setResolver, useConfig } from "kubb/kit";
2
+ import { Diagnostics, Resolver, Url, addGenerator, ast, createResolver, defineExtension, defineGenerator, setMacros, setResolver, useConfig } from "kubb/kit";
3
3
  import path, { posix } from "node:path";
4
4
  import { Const, File, Function, jsxRenderer } from "kubb/jsx";
5
5
  import { Fragment, jsx, jsxs } from "kubb/jsx/jsx-runtime";
6
- import { createFunctionParameter, createFunctionParameters, createObjectBindingPattern, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
7
6
  import { pluginZodName } from "@kubb/plugin-zod";
8
- //#region ../../internals/shared/src/params.ts
9
- /**
10
- * Drops parameters that share the same name, keeping the first.
11
- *
12
- * A malformed spec can declare the same parameter name twice within one `in` location. Both would
13
- * resolve to the same output property, so emitting both would yield an object type with a duplicate
14
- * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
15
- * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
16
- */
17
- function dedupeParams(params) {
18
- const seen = /* @__PURE__ */ new Set();
19
- return params.filter((param) => {
20
- if (seen.has(param.name)) return false;
21
- seen.add(param.name);
22
- return true;
23
- });
24
- }
25
- //#endregion
7
+ import { pluginTsName } from "@kubb/plugin-ts";
26
8
  //#region ../../internals/shared/src/operation.ts
27
9
  /**
28
10
  * Builds the `ResolverFileParams` every operation generator passes to
@@ -92,12 +74,12 @@ function getContentTypeInfo(node) {
92
74
  function getResponseContentTypeInfo(node) {
93
75
  return buildContentTypeInfo(getPrimarySuccessResponse(node)?.content?.map((e) => e.contentType) ?? []);
94
76
  }
77
+ const requestConfigBaseType = `Partial<Omit<RequestConfig, 'path' | 'query' | 'body' | 'headers' | 'url'>>`;
95
78
  function buildRequestConfigType(node) {
96
79
  const request = getContentTypeInfo(node);
97
80
  const response = getResponseContentTypeInfo(node);
98
- const configType = `Partial<Omit<RequestConfig, 'path' | 'query' | 'body' | 'headers' | 'url'>>`;
99
81
  const members = [request.isMultipleContentTypes ? `request?: ${request.contentTypeUnion}` : null, response.isMultipleContentTypes ? `response?: ${response.contentTypeUnion}` : null].filter(Boolean);
100
- return members.length ? `${configType} & { contentType?: { ${members.join("; ")} } }` : configType;
82
+ return members.length ? `${requestConfigBaseType} & { contentType?: { ${members.join("; ")} } }` : requestConfigBaseType;
101
83
  }
102
84
  /**
103
85
  * Which of the grouped request options an operation carries.
@@ -177,11 +159,19 @@ const operationParameterGroupsByNode = /* @__PURE__ */ new WeakMap();
177
159
  function getOperationParameters(node) {
178
160
  const cached = operationParameterGroupsByNode.get(node);
179
161
  if (cached) return cached;
162
+ const group = (location) => {
163
+ const seen = /* @__PURE__ */ new Set();
164
+ return node.parameters.filter((param) => {
165
+ if (param.in !== location || seen.has(param.name)) return false;
166
+ seen.add(param.name);
167
+ return true;
168
+ });
169
+ };
180
170
  const groups = {
181
- path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
182
- query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
183
- header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
184
- cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
171
+ path: group("path"),
172
+ query: group("query"),
173
+ header: group("header"),
174
+ cookie: group("cookie")
185
175
  };
186
176
  operationParameterGroupsByNode.set(node, groups);
187
177
  return groups;
@@ -203,10 +193,6 @@ function getOperationSuccessResponses(node) {
203
193
  function getPrimarySuccessResponse(node) {
204
194
  return getOperationSuccessResponses(node)[0] ?? null;
205
195
  }
206
- function findSuccessStatusCode(responses) {
207
- for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
208
- return null;
209
- }
210
196
  //#endregion
211
197
  //#region ../../internals/utils/src/casing.ts
212
198
  /**
@@ -251,7 +237,6 @@ function toSlash(p) {
251
237
  * ```
252
238
  */
253
239
  function getRelativePath(rootDir, filePath) {
254
- if (!rootDir || !filePath) throw new Error(`Root and file should be filled in when retrieving the relativePath, ${rootDir || ""} ${filePath || ""}`);
255
240
  const relativePath = posix.relative(toSlash(rootDir), toSlash(filePath));
256
241
  return relativePath.startsWith("../") ? relativePath : `./${relativePath}`;
257
242
  }
@@ -295,7 +280,6 @@ const requestGroupOrder = [
295
280
  "body"
296
281
  ];
297
282
  function McpHandler({ name, clientName, node, resolver, returnType = "full" }) {
298
- if (!ast.isHttpOperationNode(node)) return null;
299
283
  const { signature, groups } = buildRequestParamsSignature(node, resolver, { isConfigurable: false });
300
284
  const paramsSignature = [signature, "ctx: ServerContext"].filter(Boolean).join(", ");
301
285
  const callConfig = `{ ${[
@@ -333,72 +317,31 @@ function McpHandler({ name, clientName, node, resolver, returnType = "full" }) {
333
317
  });
334
318
  }
335
319
  //#endregion
336
- //#region src/utils.ts
337
- /**
338
- * Render a group param value — compose individual schemas into `z.object({ ... })`,
339
- * or use a schema name string directly.
340
- */
341
- function zodGroupExpr(entry) {
342
- if (typeof entry === "string") return entry;
343
- return `z.object({ ${entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
344
- }
320
+ //#region src/components/Server.tsx
345
321
  /**
346
- * Convert a SchemaNode type to an inline Zod expression string.
347
- * Used as fallback when no named zod schema is available for a path parameter.
322
+ * Composes individual param schemas into `z.object({ ... })`.
348
323
  */
349
- function zodExprFromSchemaNode(schema) {
350
- const baseExpr = (() => {
351
- if (schema.type === "enum") {
352
- const rawValues = schema.namedEnumValues?.length ? schema.namedEnumValues.map((v) => v.value) : (schema.enumValues ?? []).filter((v) => v !== null);
353
- if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) return `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
354
- if (rawValues.length > 0) {
355
- const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`);
356
- return literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
357
- }
358
- return "z.string()";
359
- }
360
- if (schema.type === "integer") return "z.coerce.number()";
361
- if (schema.type === "number") return "z.number()";
362
- if (schema.type === "boolean") return "z.boolean()";
363
- if (schema.type === "array") return "z.array(z.unknown())";
364
- return "z.string()";
365
- })();
366
- return schema.nullable ? `${baseExpr}.nullable()` : baseExpr;
367
- }
368
- //#endregion
369
- //#region src/components/Server.tsx
370
- const keysPrinter = functionPrinter({ mode: "call" });
324
+ const zodObject = (params) => `z.object({ ${params.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
371
325
  function Server({ serverName, serverVersion, operations }) {
372
- const registrations = operations.map(({ tool, mcp, zod, node }) => {
373
- const { path: pathParams } = getOperationParameters(node);
326
+ const registrations = operations.map(({ tool, mcp, zod }) => {
374
327
  const entries = [];
375
- if (pathParams.length) {
376
- const pathFields = pathParams.map((p) => {
377
- const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
378
- return `${JSON.stringify(p.name)}: ${zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)}`;
379
- }).join(", ");
380
- entries.push({
381
- key: "path",
382
- value: `z.object({ ${pathFields} })`
383
- });
384
- }
328
+ if (zod.pathParams.length) entries.push({
329
+ key: "path",
330
+ value: zodObject(zod.pathParams)
331
+ });
385
332
  if (zod.queryParams) entries.push({
386
333
  key: "query",
387
- value: zodGroupExpr(zod.queryParams)
334
+ value: zodObject(zod.queryParams)
388
335
  });
389
336
  if (zod.headerParams) entries.push({
390
337
  key: "headers",
391
- value: zodGroupExpr(zod.headerParams)
338
+ value: zodObject(zod.headerParams)
392
339
  });
393
340
  if (zod.requestName) entries.push({
394
341
  key: "body",
395
342
  value: zod.requestName
396
343
  });
397
- const paramsNode = entries.length ? createFunctionParameters({ params: [createFunctionParameter({
398
- name: createObjectBindingPattern({ elements: entries.map((e) => ({ name: e.key })) }),
399
- optional: false
400
- })] }) : null;
401
- const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
344
+ const destructured = `{ ${entries.map((e) => e.key).join(", ")} }`;
402
345
  const inputSchema = entries.length ? `z.object({ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} })` : null;
403
346
  const outputSchema = zod.responseName;
404
347
  const config = [
@@ -506,7 +449,7 @@ const contractPlugins = [
506
449
  * @example
507
450
  * ```ts
508
451
  * resolveClient({ client: undefined, pluginNames: ['plugin-ts'] })
509
- * // { kind: 'error', message: 'No client plugin is registered…' }
452
+ * // { kind: 'error', code: 'KUBB_MISSING_DEPENDENCY', message: 'No client plugin is registered.', help: 'Add …' }
510
453
  * ```
511
454
  */
512
455
  function resolveClient(options) {
@@ -516,7 +459,9 @@ function resolveClient(options) {
516
459
  const pluginName = selectorToPlugin[client];
517
460
  if (!has(pluginName)) return {
518
461
  kind: "error",
519
- message: `\`client: '${client}'\` is set but \`@kubb/plugin-${client}\` is not registered in \`plugins\`. Add it, or drop \`client\` to use a different client plugin.`
462
+ code: Diagnostics.code.missingDependency,
463
+ message: `\`client: '${client}'\` is set but \`@kubb/plugin-${client}\` is not registered in \`plugins\`.`,
464
+ help: `Add \`@kubb/plugin-${client}\` to \`plugins\`, or drop \`client\` to use a different client plugin.`
520
465
  };
521
466
  return {
522
467
  kind: "contract",
@@ -525,7 +470,9 @@ function resolveClient(options) {
525
470
  }
526
471
  if (client !== void 0) return {
527
472
  kind: "error",
528
- message: `Option \`client\` is ${typeof client === "string" ? `'${client}'` : String(client)}, but it must be one of 'fetch', 'axios', 'client'. Drop \`client\` to use the registered client plugin.`
473
+ code: Diagnostics.code.invalidPluginOptions,
474
+ message: `Option \`client\` is ${typeof client === "string" ? `'${client}'` : String(client)}, but it must be one of 'fetch', 'axios', 'client'.`,
475
+ help: "Set `client` to one of those values, or drop it to use the registered client plugin."
529
476
  };
530
477
  const registered = contractPlugins.filter(has);
531
478
  if (registered.length === 1) return {
@@ -534,25 +481,40 @@ function resolveClient(options) {
534
481
  };
535
482
  if (registered.length > 1) return {
536
483
  kind: "error",
537
- message: `Multiple client plugins are registered (${registered.map((name) => `\`@kubb/${name}\``).join(", ")}). Set \`client: 'fetch' | 'axios' | 'client'\` to choose which client the hooks call, or register a single client plugin.`
484
+ code: Diagnostics.code.invalidPluginOptions,
485
+ message: `Multiple client plugins are registered (${registered.map((name) => `\`@kubb/${name}\``).join(", ")}) and \`client\` is not set.`,
486
+ help: "Set `client: 'fetch' | 'axios' | 'client'` to choose which client the hooks call, or register a single client plugin."
538
487
  };
539
488
  return {
540
489
  kind: "error",
541
- message: "No client plugin is registered. Add `@kubb/plugin-axios`, `@kubb/plugin-fetch`, or `@kubb/plugin-client` to `plugins` so the generated code has an HTTP client to call."
490
+ code: Diagnostics.code.missingDependency,
491
+ message: "No client plugin is registered.",
492
+ help: "Add `@kubb/plugin-fetch`, `@kubb/plugin-axios`, or `@kubb/plugin-client` to `plugins` so the generated code has an HTTP client to call."
542
493
  };
543
494
  }
544
495
  /**
545
- * Resolves the contract client during a consumer plugin's setup hook. Extracts the plugin names
546
- * from the raw `plugins` config, applies {@link resolveClient}, and throws the diagnostic on a
547
- * misconfiguration so every consumer fails fast with the same message.
496
+ * Resolves the contract client during a consumer plugin's setup hook.
497
+ * It extracts the plugin names from the raw `plugins` config and applies {@link resolveClient}.
498
+ * On a misconfiguration it throws a config diagnostic tagged with `plugin`, so every consumer
499
+ * fails fast with the same message.
548
500
  */
549
501
  function resolveContractClient(options) {
550
- const { client, plugins = [] } = options;
502
+ const { plugin, client, plugins = [] } = options;
551
503
  const resolved = resolveClient({
552
504
  client,
553
505
  pluginNames: plugins.map((plugin) => plugin.name).filter((name) => Boolean(name))
554
506
  });
555
- if (resolved.kind === "error") throw new Error(resolved.message);
507
+ if (resolved.kind === "error") {
508
+ const { code, message, help } = resolved;
509
+ throw new Diagnostics.Error({
510
+ code,
511
+ severity: "error",
512
+ message,
513
+ help,
514
+ location: { kind: "config" },
515
+ plugin
516
+ });
517
+ }
556
518
  return resolved;
557
519
  }
558
520
  //#endregion
@@ -645,10 +607,7 @@ const mcpGenerator = defineGenerator({
645
607
  const meta = {
646
608
  name: resolver.handler.name(node),
647
609
  file: resolver.file({
648
- name: node.operationId,
649
- extname: ".ts",
650
- tag: node.tags[0] ?? "default",
651
- path: node.path,
610
+ ...operationFileEntry(node, node.operationId),
652
611
  root,
653
612
  output,
654
613
  group: group ?? void 0
@@ -727,22 +686,14 @@ const serverGenerator = defineGenerator({
727
686
  const { output, group } = ctx.options;
728
687
  const pluginZod = ctx.requirePlugin(pluginZodName);
729
688
  const zodResolver = driver.getResolver(pluginZodName);
730
- const name = "server";
731
- const serverFile = {
732
- baseName: "server.ts",
733
- path: path.resolve(root, output.path, "server.ts"),
734
- meta: { pluginName: plugin.name }
735
- };
736
- const startFile = {
737
- baseName: "start.ts",
738
- path: path.resolve(root, output.path, "start.ts"),
739
- meta: { pluginName: plugin.name }
740
- };
741
- const jsonFile = {
742
- baseName: ".mcp.json",
743
- path: path.resolve(root, output.path, ".mcp.json"),
689
+ const fileAt = (baseName) => ({
690
+ baseName,
691
+ path: path.resolve(root, output.path, baseName),
744
692
  meta: { pluginName: plugin.name }
745
- };
693
+ });
694
+ const serverFile = fileAt("server.ts");
695
+ const startFile = fileAt("start.ts");
696
+ const jsonFile = fileAt(".mcp.json");
746
697
  const mcpConfig = { mcpServers: { [ctx.meta.title || "server"]: {
747
698
  type: "stdio",
748
699
  command: "npx",
@@ -750,26 +701,21 @@ const serverGenerator = defineGenerator({
750
701
  } } };
751
702
  const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
752
703
  const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
704
+ const fileEntry = operationFileEntry(node, node.operationId);
753
705
  const mcpFile = resolver.file({
754
- name: node.operationId,
755
- extname: ".ts",
756
- tag: node.tags[0] ?? "default",
757
- path: node.path,
706
+ ...fileEntry,
758
707
  root,
759
708
  output,
760
709
  group: group ?? void 0
761
710
  });
762
711
  const zodFile = zodResolver.file({
763
- name: node.operationId,
764
- extname: ".ts",
765
- tag: node.tags[0] ?? "default",
766
- path: node.path,
712
+ ...fileEntry,
767
713
  root,
768
714
  output: pluginZod.options?.output ?? output,
769
715
  group: pluginZod.options?.group ?? void 0
770
716
  });
771
717
  const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.response.body(node) : null;
772
- const successStatus = findSuccessStatusCode(node.responses);
718
+ const successStatus = getPrimarySuccessResponse(node)?.statusCode;
773
719
  const responseName = successStatus ? zodResolver.response.status(node, successStatus) : null;
774
720
  const resolveParams = (params) => params.map((p) => ({
775
721
  name: p.name,
@@ -792,8 +738,7 @@ const serverGenerator = defineGenerator({
792
738
  requestName,
793
739
  responseName,
794
740
  file: zodFile
795
- },
796
- node
741
+ }
797
742
  };
798
743
  });
799
744
  const imports = operationsMapped.flatMap(({ mcp, zod }) => {
@@ -891,7 +836,7 @@ const serverGenerator = defineGenerator({
891
836
  path: jsonFile.path,
892
837
  meta: jsonFile.meta,
893
838
  children: /* @__PURE__ */ jsx(File.Source, {
894
- name,
839
+ name: "server",
895
840
  children: `${JSON.stringify(mcpConfig, null, 2)}\n`
896
841
  })
897
842
  })
@@ -906,7 +851,7 @@ var compatibility = {
906
851
  };
907
852
  //#endregion
908
853
  //#region package.json
909
- var version = "5.2.0";
854
+ var version = "5.2.1";
910
855
  //#endregion
911
856
  //#region src/resolvers/resolverMcp.ts
912
857
  /**
@@ -992,6 +937,7 @@ const pluginMcp = defineExtension({
992
937
  override,
993
938
  group: createGroupConfig(group),
994
939
  client: resolveContractClient({
940
+ plugin: pluginMcpName,
995
941
  client,
996
942
  plugins: useConfig().plugins
997
943
  }),