@kubb/plugin-mcp 5.2.0-canary.20261014T071450 → 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 = [
@@ -470,23 +413,30 @@ return server`
470
413
  * operation, shared so the selection rules and diagnostics stay in one place.
471
414
  *
472
415
  * Every client runtime lives in a dedicated client plugin, so a consumer always calls a registered
473
- * contract client plugin (plugin-fetch or plugin-axios) and never emits its own inline client:
416
+ * contract client plugin (plugin-fetch, plugin-axios, or plugin-client) and never emits its own
417
+ * inline client:
474
418
  *
475
419
  * - `contract` — a registered contract client plugin owns the `<op>` functions and the consumer
476
420
  * imports and calls them.
477
421
  * - `error` — no client plugin is registered, several are registered without a selector, the
478
422
  * requested one is missing, or `client` is not a known selector.
479
423
  *
480
- * The `client` string selects explicitly (`'fetch'` / `'axios'`); when it is unset a lone registered
481
- * contract client plugin is picked up automatically.
424
+ * The `client` string selects explicitly (`'fetch'` / `'axios'` / `'client'`); when it is unset a
425
+ * lone registered contract client plugin is picked up automatically.
482
426
  */
483
427
  const pluginFetchName = "plugin-fetch";
484
428
  const pluginAxiosName = "plugin-axios";
429
+ const pluginClientName = "plugin-client";
485
430
  const selectorToPlugin = {
486
431
  fetch: pluginFetchName,
487
- axios: pluginAxiosName
432
+ axios: pluginAxiosName,
433
+ client: pluginClientName
488
434
  };
489
- const contractPlugins = [pluginFetchName, pluginAxiosName];
435
+ const contractPlugins = [
436
+ pluginFetchName,
437
+ pluginAxiosName,
438
+ pluginClientName
439
+ ];
490
440
  /**
491
441
  * Applies the `client` resolution rules. See the module comment for the strategy shape.
492
442
  *
@@ -499,17 +449,19 @@ const contractPlugins = [pluginFetchName, pluginAxiosName];
499
449
  * @example
500
450
  * ```ts
501
451
  * resolveClient({ client: undefined, pluginNames: ['plugin-ts'] })
502
- * // { kind: 'error', message: 'No client plugin is registered…' }
452
+ * // { kind: 'error', code: 'KUBB_MISSING_DEPENDENCY', message: 'No client plugin is registered.', help: 'Add …' }
503
453
  * ```
504
454
  */
505
455
  function resolveClient(options) {
506
456
  const { client, pluginNames } = options;
507
457
  const has = (name) => pluginNames.includes(name);
508
- if (client === "fetch" || client === "axios") {
458
+ if (client === "fetch" || client === "axios" || client === "client") {
509
459
  const pluginName = selectorToPlugin[client];
510
460
  if (!has(pluginName)) return {
511
461
  kind: "error",
512
- 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.`
513
465
  };
514
466
  return {
515
467
  kind: "contract",
@@ -518,7 +470,9 @@ function resolveClient(options) {
518
470
  }
519
471
  if (client !== void 0) return {
520
472
  kind: "error",
521
- message: `Option \`client\` is ${typeof client === "string" ? `'${client}'` : String(client)}, but it must be one of 'fetch', 'axios'. 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."
522
476
  };
523
477
  const registered = contractPlugins.filter(has);
524
478
  if (registered.length === 1) return {
@@ -527,36 +481,59 @@ function resolveClient(options) {
527
481
  };
528
482
  if (registered.length > 1) return {
529
483
  kind: "error",
530
- message: `Multiple client plugins are registered (${registered.map((name) => `\`@kubb/${name}\``).join(", ")}). Set \`client: 'fetch' | 'axios'\` 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."
531
487
  };
532
488
  return {
533
489
  kind: "error",
534
- message: "No client plugin is registered. Add `@kubb/plugin-axios` or `@kubb/plugin-fetch` 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."
535
493
  };
536
494
  }
537
495
  /**
538
- * Resolves the contract client during a consumer plugin's setup hook. Extracts the plugin names
539
- * from the raw `plugins` config, applies {@link resolveClient}, and throws the diagnostic on a
540
- * 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.
541
500
  */
542
501
  function resolveContractClient(options) {
543
- const { client, plugins = [] } = options;
502
+ const { plugin, client, plugins = [] } = options;
544
503
  const resolved = resolveClient({
545
504
  client,
546
505
  pluginNames: plugins.map((plugin) => plugin.name).filter((name) => Boolean(name))
547
506
  });
548
- 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
+ }
549
518
  return resolved;
550
519
  }
551
520
  //#endregion
552
521
  //#region ../../internals/client/src/resolveClientOperation.ts
522
+ function isRelativeImport(importPath) {
523
+ return importPath.startsWith(".");
524
+ }
525
+ function resolveImportPath(options) {
526
+ const { importPath, operationFile } = options;
527
+ return isRelativeImport(importPath) ? path.resolve(path.dirname(operationFile), importPath) : importPath;
528
+ }
553
529
  /**
554
530
  * Resolves the contract client `<op>` a consumer (query hook, MCP handler) imports, by looking up
555
531
  * the registered contract client plugin's resolver and output. Works for any contract client plugin
556
- * (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
532
+ * (plugin-fetch, plugin-axios, or plugin-client). Returns `null` when no contract plugin is in play (the inline
557
533
  * path), or when the client plugin's `exclude` / `include` leaves this operation out, since it then
558
- * emits no `<op>` to import. The plugin injects `.kubb/client.ts` at the global output root, the
559
- * same path consumers read `RequestConfig` / `ResponseErrorConfig` from.
534
+ * emits no `<op>` to import. plugin-fetch / plugin-axios inject `.kubb/client.ts` at the global
535
+ * output root, the same path consumers read `RequestConfig` / `ResponseErrorConfig` from.
536
+ * With plugin-client they come from the user's `importPath` module instead.
560
537
  *
561
538
  * The result is cached in the current node's `cache` (`ctx.cache`) under the client plugin's name,
562
539
  * so several dependents reading the same client plugin for one operation in a single pass
@@ -587,10 +564,15 @@ function resolveClientOperation(options) {
587
564
  output: operationOptions.output ?? output,
588
565
  group: operationOptions.group ?? void 0
589
566
  });
567
+ const { importPath } = operationOptions;
590
568
  return {
591
569
  name: resolver.name(node.operationId),
592
570
  path: file.path,
593
- clientPath: path.resolve(root, ".kubb/client.ts"),
571
+ clientPath: importPath === void 0 ? path.resolve(root, ".kubb/client.ts") : resolveImportPath({
572
+ importPath,
573
+ operationFile: file.path
574
+ }),
575
+ clientPathIsFile: importPath === void 0 || isRelativeImport(importPath),
594
576
  returnType: operationOptions.returnType ?? "full"
595
577
  };
596
578
  });
@@ -625,10 +607,7 @@ const mcpGenerator = defineGenerator({
625
607
  const meta = {
626
608
  name: resolver.handler.name(node),
627
609
  file: resolver.file({
628
- name: node.operationId,
629
- extname: ".ts",
630
- tag: node.tags[0] ?? "default",
631
- path: node.path,
610
+ ...operationFileEntry(node, node.operationId),
632
611
  root,
633
612
  output,
634
613
  group: group ?? void 0
@@ -707,22 +686,14 @@ const serverGenerator = defineGenerator({
707
686
  const { output, group } = ctx.options;
708
687
  const pluginZod = ctx.requirePlugin(pluginZodName);
709
688
  const zodResolver = driver.getResolver(pluginZodName);
710
- const name = "server";
711
- const serverFile = {
712
- baseName: "server.ts",
713
- path: path.resolve(root, output.path, "server.ts"),
689
+ const fileAt = (baseName) => ({
690
+ baseName,
691
+ path: path.resolve(root, output.path, baseName),
714
692
  meta: { pluginName: plugin.name }
715
- };
716
- const startFile = {
717
- baseName: "start.ts",
718
- path: path.resolve(root, output.path, "start.ts"),
719
- meta: { pluginName: plugin.name }
720
- };
721
- const jsonFile = {
722
- baseName: ".mcp.json",
723
- path: path.resolve(root, output.path, ".mcp.json"),
724
- meta: { pluginName: plugin.name }
725
- };
693
+ });
694
+ const serverFile = fileAt("server.ts");
695
+ const startFile = fileAt("start.ts");
696
+ const jsonFile = fileAt(".mcp.json");
726
697
  const mcpConfig = { mcpServers: { [ctx.meta.title || "server"]: {
727
698
  type: "stdio",
728
699
  command: "npx",
@@ -730,26 +701,21 @@ const serverGenerator = defineGenerator({
730
701
  } } };
731
702
  const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
732
703
  const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
704
+ const fileEntry = operationFileEntry(node, node.operationId);
733
705
  const mcpFile = resolver.file({
734
- name: node.operationId,
735
- extname: ".ts",
736
- tag: node.tags[0] ?? "default",
737
- path: node.path,
706
+ ...fileEntry,
738
707
  root,
739
708
  output,
740
709
  group: group ?? void 0
741
710
  });
742
711
  const zodFile = zodResolver.file({
743
- name: node.operationId,
744
- extname: ".ts",
745
- tag: node.tags[0] ?? "default",
746
- path: node.path,
712
+ ...fileEntry,
747
713
  root,
748
714
  output: pluginZod.options?.output ?? output,
749
715
  group: pluginZod.options?.group ?? void 0
750
716
  });
751
717
  const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.response.body(node) : null;
752
- const successStatus = findSuccessStatusCode(node.responses);
718
+ const successStatus = getPrimarySuccessResponse(node)?.statusCode;
753
719
  const responseName = successStatus ? zodResolver.response.status(node, successStatus) : null;
754
720
  const resolveParams = (params) => params.map((p) => ({
755
721
  name: p.name,
@@ -772,8 +738,7 @@ const serverGenerator = defineGenerator({
772
738
  requestName,
773
739
  responseName,
774
740
  file: zodFile
775
- },
776
- node
741
+ }
777
742
  };
778
743
  });
779
744
  const imports = operationsMapped.flatMap(({ mcp, zod }) => {
@@ -871,7 +836,7 @@ const serverGenerator = defineGenerator({
871
836
  path: jsonFile.path,
872
837
  meta: jsonFile.meta,
873
838
  children: /* @__PURE__ */ jsx(File.Source, {
874
- name,
839
+ name: "server",
875
840
  children: `${JSON.stringify(mcpConfig, null, 2)}\n`
876
841
  })
877
842
  })
@@ -886,7 +851,7 @@ var compatibility = {
886
851
  };
887
852
  //#endregion
888
853
  //#region package.json
889
- var version = "5.2.0-canary.20261014T071450";
854
+ var version = "5.2.1";
890
855
  //#endregion
891
856
  //#region src/resolvers/resolverMcp.ts
892
857
  /**
@@ -972,6 +937,7 @@ const pluginMcp = defineExtension({
972
937
  override,
973
938
  group: createGroupConfig(group),
974
939
  client: resolveContractClient({
940
+ plugin: pluginMcpName,
975
941
  client,
976
942
  plugins: useConfig().plugins
977
943
  }),