mcp-from-openapi 2.2.0 → 2.4.0

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/index.js CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BUILTIN_FORMAT_RESOLVERS: () => BUILTIN_FORMAT_RESOLVERS,
33
34
  GenerationError: () => GenerationError,
34
35
  LoadError: () => LoadError,
35
36
  OpenAPIToolError: () => OpenAPIToolError,
@@ -44,15 +45,13 @@ __export(index_exports, {
44
45
  Validator: () => Validator,
45
46
  createSecurityContext: () => createSecurityContext,
46
47
  isReferenceObject: () => isReferenceObject,
48
+ resolveSchemaFormats: () => resolveSchemaFormats,
47
49
  toJsonSchema: () => toJsonSchema
48
50
  });
49
51
  module.exports = __toCommonJS(index_exports);
50
52
 
51
53
  // src/generator.ts
52
54
  var yaml = __toESM(require("yaml"));
53
- var path = __toESM(require("path"));
54
- var fs = __toESM(require("fs/promises"));
55
- var import_json_schema_ref_parser = __toESM(require("@apidevtools/json-schema-ref-parser"));
56
55
 
57
56
  // src/types.ts
58
57
  function isReferenceObject(obj) {
@@ -587,12 +586,12 @@ var Validator = class {
587
586
  * Validate paths
588
587
  */
589
588
  validatePaths(paths, errors, warnings) {
590
- for (const [path2, pathItem] of Object.entries(paths)) {
589
+ for (const [path, pathItem] of Object.entries(paths)) {
591
590
  if (!pathItem) continue;
592
- if (!path2.startsWith("/")) {
591
+ if (!path.startsWith("/")) {
593
592
  errors.push({
594
- message: `Path must start with '/': ${path2}`,
595
- path: `/paths/${path2}`,
593
+ message: `Path must start with '/': ${path}`,
594
+ path: `/paths/${path}`,
596
595
  code: "INVALID_PATH_FORMAT"
597
596
  });
598
597
  }
@@ -602,13 +601,13 @@ var Validator = class {
602
601
  const operation = pathItem[method];
603
602
  if (operation) {
604
603
  hasOperations = true;
605
- this.validateOperation(operation, path2, method, errors, warnings);
604
+ this.validateOperation(operation, path, method, errors, warnings);
606
605
  }
607
606
  }
608
607
  if (!hasOperations && !pathItem.$ref) {
609
608
  warnings.push({
610
- message: `Path has no operations: ${path2}`,
611
- path: `/paths/${path2}`,
609
+ message: `Path has no operations: ${path}`,
610
+ path: `/paths/${path}`,
612
611
  code: "NO_OPERATIONS"
613
612
  });
614
613
  }
@@ -617,33 +616,33 @@ var Validator = class {
617
616
  /**
618
617
  * Validate an operation
619
618
  */
620
- validateOperation(operation, path2, method, errors, warnings) {
621
- const basePath = `/paths/${path2}/${method}`;
619
+ validateOperation(operation, path, method, errors, warnings) {
620
+ const basePath = `/paths/${path}/${method}`;
622
621
  if (!operation.operationId) {
623
622
  warnings.push({
624
- message: `Operation missing operationId: ${method.toUpperCase()} ${path2}`,
623
+ message: `Operation missing operationId: ${method.toUpperCase()} ${path}`,
625
624
  path: `${basePath}/operationId`,
626
625
  code: "NO_OPERATION_ID"
627
626
  });
628
627
  }
629
628
  if (!operation.responses || Object.keys(operation.responses).length === 0) {
630
629
  errors.push({
631
- message: `Operation missing responses: ${method.toUpperCase()} ${path2}`,
630
+ message: `Operation missing responses: ${method.toUpperCase()} ${path}`,
632
631
  path: `${basePath}/responses`,
633
632
  code: "NO_RESPONSES"
634
633
  });
635
634
  }
636
635
  if (operation.parameters) {
637
- this.validateParameters(operation.parameters, path2, method, errors, warnings);
636
+ this.validateParameters(operation.parameters, path, method, errors, warnings);
638
637
  }
639
- const pathParams = path2.match(/\{([^}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
638
+ const pathParams = path.match(/\{([^}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
640
639
  const definedPathParams = new Set(
641
640
  operation.parameters?.filter((p) => p.in === "path").map((p) => p.name) ?? []
642
641
  );
643
642
  for (const param of pathParams) {
644
643
  if (!definedPathParams.has(param)) {
645
644
  errors.push({
646
- message: `Path parameter '${param}' not defined in parameters: ${method.toUpperCase()} ${path2}`,
645
+ message: `Path parameter '${param}' not defined in parameters: ${method.toUpperCase()} ${path}`,
647
646
  path: `${basePath}/parameters`,
648
647
  code: "MISSING_PATH_PARAMETER"
649
648
  });
@@ -653,8 +652,8 @@ var Validator = class {
653
652
  /**
654
653
  * Validate parameters
655
654
  */
656
- validateParameters(parameters, path2, method, errors, warnings) {
657
- const basePath = `/paths/${path2}/${method}/parameters`;
655
+ validateParameters(parameters, path, method, errors, warnings) {
656
+ const basePath = `/paths/${path}/${method}/parameters`;
658
657
  for (let i = 0; i < parameters.length; i++) {
659
658
  const param = parameters[i];
660
659
  const paramPath = `${basePath}/${i}`;
@@ -736,6 +735,115 @@ var SchemaError = class extends OpenAPIToolError {
736
735
  }
737
736
  };
738
737
 
738
+ // src/format-resolver.ts
739
+ var BUILTIN_FORMAT_RESOLVERS = {
740
+ // String formats
741
+ uuid: (schema) => ({
742
+ ...schema,
743
+ pattern: schema.pattern ?? "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
744
+ description: schema.description || "UUID string (RFC 4122)"
745
+ }),
746
+ "date-time": (schema) => ({
747
+ ...schema,
748
+ description: schema.description || "ISO 8601 date-time (e.g., 2024-01-15T09:30:00Z)"
749
+ }),
750
+ date: (schema) => ({
751
+ ...schema,
752
+ pattern: schema.pattern ?? "^\\d{4}-\\d{2}-\\d{2}$",
753
+ description: schema.description || "ISO 8601 date (e.g., 2024-01-15)"
754
+ }),
755
+ time: (schema) => ({
756
+ ...schema,
757
+ pattern: schema.pattern ?? "^\\d{2}:\\d{2}:\\d{2}",
758
+ description: schema.description || "ISO 8601 time (e.g., 09:30:00)"
759
+ }),
760
+ email: (schema) => ({
761
+ ...schema,
762
+ description: schema.description || "Email address (RFC 5322)"
763
+ }),
764
+ uri: (schema) => ({
765
+ ...schema,
766
+ description: schema.description || "URI (RFC 3986)"
767
+ }),
768
+ "uri-reference": (schema) => ({
769
+ ...schema,
770
+ description: schema.description || "URI reference (RFC 3986)"
771
+ }),
772
+ hostname: (schema) => ({
773
+ ...schema,
774
+ description: schema.description || "Internet hostname (RFC 1123)"
775
+ }),
776
+ ipv4: (schema) => ({
777
+ ...schema,
778
+ pattern: schema.pattern ?? "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$",
779
+ description: schema.description || "IPv4 address"
780
+ }),
781
+ ipv6: (schema) => ({
782
+ ...schema,
783
+ description: schema.description || "IPv6 address (RFC 4291)"
784
+ }),
785
+ // Integer formats
786
+ int32: (schema) => ({
787
+ ...schema,
788
+ minimum: schema.minimum ?? -2147483648,
789
+ maximum: schema.maximum ?? 2147483647
790
+ }),
791
+ int64: (schema) => ({
792
+ ...schema,
793
+ minimum: schema.minimum ?? Number.MIN_SAFE_INTEGER,
794
+ maximum: schema.maximum ?? Number.MAX_SAFE_INTEGER
795
+ }),
796
+ // Binary/encoding formats
797
+ byte: (schema) => ({
798
+ ...schema,
799
+ pattern: schema.pattern ?? "^[A-Za-z0-9+/]*={0,2}$",
800
+ description: schema.description || "Base64-encoded string (RFC 4648)"
801
+ }),
802
+ binary: (schema) => ({
803
+ ...schema,
804
+ description: schema.description || "Binary data"
805
+ }),
806
+ // Sensitive data formats
807
+ password: (schema) => ({
808
+ ...schema,
809
+ description: schema.description || "Password (sensitive, UI should mask input)"
810
+ })
811
+ };
812
+ function resolveSchemaFormats(schema, resolvers) {
813
+ if (!schema || typeof schema !== "object") return schema;
814
+ let result = { ...schema };
815
+ const format = result["format"];
816
+ if (format && resolvers[format]) {
817
+ result = { ...resolvers[format](result) };
818
+ }
819
+ if (result["properties"] && typeof result["properties"] === "object") {
820
+ const props = {};
821
+ for (const [key, value] of Object.entries(result["properties"])) {
822
+ props[key] = resolveSchemaFormats(value, resolvers);
823
+ }
824
+ result["properties"] = props;
825
+ }
826
+ if (result["items"]) {
827
+ if (Array.isArray(result["items"])) {
828
+ result["items"] = result["items"].map((item) => resolveSchemaFormats(item, resolvers));
829
+ } else {
830
+ result["items"] = resolveSchemaFormats(result["items"], resolvers);
831
+ }
832
+ }
833
+ if (result["additionalProperties"] && typeof result["additionalProperties"] === "object") {
834
+ result["additionalProperties"] = resolveSchemaFormats(result["additionalProperties"], resolvers);
835
+ }
836
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
837
+ if (result[key] && Array.isArray(result[key])) {
838
+ result[key] = result[key].map((s) => resolveSchemaFormats(s, resolvers));
839
+ }
840
+ }
841
+ if (result["not"] && typeof result["not"] === "object") {
842
+ result["not"] = resolveSchemaFormats(result["not"], resolvers);
843
+ }
844
+ return result;
845
+ }
846
+
739
847
  // src/generator.ts
740
848
  var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
741
849
  document;
@@ -800,6 +908,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
800
908
  */
801
909
  static async fromFile(filePath, options = {}) {
802
910
  try {
911
+ const [path, fs] = await Promise.all([import("path"), import("fs/promises")]);
803
912
  const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
804
913
  const content = await fs.readFile(absolutePath, "utf-8");
805
914
  const ext = path.extname(filePath).toLowerCase();
@@ -890,6 +999,31 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
890
999
  /^\[fe80:/i
891
1000
  // bracketed IPv6 link-local
892
1001
  ];
1002
+ /**
1003
+ * Decode an IPv4-mapped IPv6 host (`::ffff:169.254.169.254` or its hex form
1004
+ * `::ffff:a9fe:a9fe`, optionally bracketed) to its embedded dotted-quad IPv4,
1005
+ * or `null` if the host isn't IPv4-mapped. `new URL().hostname` normalizes
1006
+ * `[::ffff:169.254.169.254]` to `[::ffff:a9fe:a9fe]`, which the plain
1007
+ * dotted-quad blocklist patterns miss — letting an attacker reach a private /
1008
+ * metadata IPv4 through the v6 mapping. We re-check the decoded v4.
1009
+ */
1010
+ static mappedIPv4FromIPv6(hostname) {
1011
+ let h = hostname;
1012
+ if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
1013
+ const lower = h.toLowerCase();
1014
+ const marker = lower.lastIndexOf("::ffff:");
1015
+ if (marker === -1) return null;
1016
+ const tail = lower.slice(marker + "::ffff:".length);
1017
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(tail)) return tail;
1018
+ const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
1019
+ if (hex) {
1020
+ const hi = parseInt(hex[1], 16);
1021
+ const lo = parseInt(hex[2], 16);
1022
+ if (Number.isNaN(hi) || Number.isNaN(lo)) return null;
1023
+ return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
1024
+ }
1025
+ return null;
1026
+ }
893
1027
  /**
894
1028
  * Check whether a hostname is blocked (internal/private IP or explicit blocklist).
895
1029
  */
@@ -900,11 +1034,16 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
900
1034
  if (refOpts.blockedHosts.includes(hostname)) {
901
1035
  return true;
902
1036
  }
903
- for (const pattern of _OpenAPIToolGenerator.BLOCKED_HOSTNAME_PATTERNS) {
904
- if (typeof pattern === "string") {
905
- if (hostname === pattern) return true;
906
- } else {
907
- if (pattern.test(hostname)) return true;
1037
+ const candidates = [hostname];
1038
+ const mapped = _OpenAPIToolGenerator.mappedIPv4FromIPv6(hostname);
1039
+ if (mapped) candidates.push(mapped);
1040
+ for (const candidate of candidates) {
1041
+ for (const pattern of _OpenAPIToolGenerator.BLOCKED_HOSTNAME_PATTERNS) {
1042
+ if (typeof pattern === "string") {
1043
+ if (candidate === pattern) return true;
1044
+ } else {
1045
+ if (pattern.test(candidate)) return true;
1046
+ }
908
1047
  }
909
1048
  }
910
1049
  return false;
@@ -934,6 +1073,14 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
934
1073
  const hasHostAllowlist = refOpts.allowedHosts.length > 0;
935
1074
  const hostAllowSet = new Set(refOpts.allowedHosts);
936
1075
  resolveConfig["http"] = {
1076
+ // SECURITY: never auto-follow HTTP redirects when resolving external
1077
+ // `$ref`s. `canRead` (below) validates only the INITIAL URL; the
1078
+ // resolver's default redirect-following (up to 5 hops) re-fetches the
1079
+ // `Location` target WITHOUT re-invoking `canRead`, so an allowlisted host
1080
+ // could 302 → `http://169.254.169.254/...` and smuggle a blocked target
1081
+ // past the allowlist/blocklist. Setting `redirects: 0` refuses the first
1082
+ // redirect; legitimate refs resolve in one hop.
1083
+ redirects: 0,
937
1084
  canRead: (file) => {
938
1085
  try {
939
1086
  const parsed = new URL(file.url);
@@ -959,29 +1106,84 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
959
1106
  return { resolve: resolveConfig };
960
1107
  }
961
1108
  /**
962
- * Initialize the generator (dereference if needed)
1109
+ * Does the document contain any EXTERNAL `$ref` (a ref that is not a local
1110
+ * JSON-pointer beginning with `#`)? Only external refs require the full
1111
+ * `$RefParser` (file/http resolvers, which pull Node builtins). A document
1112
+ * with only internal refs can be dereferenced with the runtime-agnostic
1113
+ * resolver below — so it works on V8 isolates (Cloudflare Workers) too.
1114
+ */
1115
+ static hasExternalRefs(node, seen = /* @__PURE__ */ new Set()) {
1116
+ if (node === null || typeof node !== "object") return false;
1117
+ if (seen.has(node)) return false;
1118
+ seen.add(node);
1119
+ if (Array.isArray(node)) return node.some((n) => _OpenAPIToolGenerator.hasExternalRefs(n, seen));
1120
+ const ref = node.$ref;
1121
+ if (typeof ref === "string" && !ref.startsWith("#")) return true;
1122
+ return Object.values(node).some(
1123
+ (v) => _OpenAPIToolGenerator.hasExternalRefs(v, seen)
1124
+ );
1125
+ }
1126
+ /**
1127
+ * Dereference local (`#/...`) `$ref`s without `$RefParser` — pure, dependency-
1128
+ * free, runtime-agnostic. A pointer cache makes circular schemas resolve to a
1129
+ * shared reference instead of recursing forever (same contract as `$RefParser`).
1130
+ */
1131
+ static dereferenceInternal(root) {
1132
+ const cache = /* @__PURE__ */ new Map();
1133
+ const resolvePointer = (ptr) => {
1134
+ const parts = ptr.replace(/^#\/?/, "").split("/").filter((p) => p.length > 0).map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
1135
+ let cur = root;
1136
+ for (const p of parts) cur = cur?.[p];
1137
+ return cur;
1138
+ };
1139
+ const walk = (node) => {
1140
+ if (node === null || typeof node !== "object") return node;
1141
+ if (Array.isArray(node)) return node.map(walk);
1142
+ const ref = node.$ref;
1143
+ if (typeof ref === "string" && ref.startsWith("#")) {
1144
+ const cached = cache.get(ref);
1145
+ if (cached !== void 0) return cached;
1146
+ const placeholder = {};
1147
+ cache.set(ref, placeholder);
1148
+ const resolved = walk(resolvePointer(ref));
1149
+ if (resolved && typeof resolved === "object") Object.assign(placeholder, resolved);
1150
+ return placeholder;
1151
+ }
1152
+ const out = {};
1153
+ for (const [k, v] of Object.entries(node)) out[k] = walk(v);
1154
+ return out;
1155
+ };
1156
+ return walk(root);
1157
+ }
1158
+ /**
1159
+ * Initialize the generator (dereference if needed, then validate)
963
1160
  */
964
1161
  async initialize() {
1162
+ if (this.options.dereference && !this.dereferencedDocument) {
1163
+ const cloned = JSON.parse(JSON.stringify(this.document));
1164
+ if (!_OpenAPIToolGenerator.hasExternalRefs(cloned)) {
1165
+ this.dereferencedDocument = _OpenAPIToolGenerator.dereferenceInternal(cloned);
1166
+ } else {
1167
+ try {
1168
+ const { default: $RefParser } = await import("@apidevtools/json-schema-ref-parser");
1169
+ const refParserOptions = this.buildRefParserOptions();
1170
+ this.dereferencedDocument = await $RefParser.dereference(cloned, refParserOptions);
1171
+ } catch (error) {
1172
+ const errorMessage = error instanceof Error ? error.message : String(error);
1173
+ throw new ParseError(`Failed to dereference OpenAPI document: ${errorMessage}`, {
1174
+ originalError: error
1175
+ });
1176
+ }
1177
+ }
1178
+ }
965
1179
  if (this.options.validate) {
966
- const result = await this.validate();
1180
+ const validator = new Validator();
1181
+ const documentToValidate = this.dereferencedDocument ?? this.document;
1182
+ const result = await validator.validate(documentToValidate);
967
1183
  if (!result.valid) {
968
1184
  throw new ParseError("Invalid OpenAPI document", { errors: result.errors });
969
1185
  }
970
1186
  }
971
- if (this.options.dereference && !this.dereferencedDocument) {
972
- try {
973
- const refParserOptions = this.buildRefParserOptions();
974
- this.dereferencedDocument = await import_json_schema_ref_parser.default.dereference(
975
- JSON.parse(JSON.stringify(this.document)),
976
- refParserOptions
977
- );
978
- } catch (error) {
979
- const errorMessage = error instanceof Error ? error.message : String(error);
980
- throw new ParseError(`Failed to dereference OpenAPI document: ${errorMessage}`, {
981
- originalError: error
982
- });
983
- }
984
- }
985
1187
  }
986
1188
  /**
987
1189
  * Generate all tools from the OpenAPI specification
@@ -1050,11 +1252,18 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1050
1252
  const name = this.generateToolName(pathStr, method, operation.operationId, options);
1051
1253
  const description = operation.summary || operation.description || `${method.toUpperCase()} ${pathStr}`;
1052
1254
  const metadata = this.extractMetadata(pathStr, method, operation, document, outputSchema);
1255
+ const formatResolvers = {
1256
+ ...options.resolveFormats ? BUILTIN_FORMAT_RESOLVERS : {},
1257
+ ...options.formatResolvers
1258
+ };
1259
+ const hasFormatResolvers = Object.keys(formatResolvers).length > 0;
1260
+ const resolvedInputSchema = hasFormatResolvers ? resolveSchemaFormats(inputSchema, formatResolvers) : inputSchema;
1261
+ const resolvedOutputSchema = hasFormatResolvers && outputSchema ? resolveSchemaFormats(outputSchema, formatResolvers) : outputSchema;
1053
1262
  return {
1054
1263
  name,
1055
1264
  description,
1056
- inputSchema,
1057
- outputSchema,
1265
+ inputSchema: resolvedInputSchema,
1266
+ outputSchema: resolvedOutputSchema,
1058
1267
  mapper,
1059
1268
  metadata
1060
1269
  };
@@ -1062,7 +1271,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1062
1271
  /**
1063
1272
  * Check if an operation should be included
1064
1273
  */
1065
- shouldIncludeOperation(operation, path2, method, options) {
1274
+ shouldIncludeOperation(operation, path, method, options) {
1066
1275
  if (operation.deprecated && !options.includeDeprecated) {
1067
1276
  return false;
1068
1277
  }
@@ -1079,7 +1288,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1079
1288
  if (options.filterFn) {
1080
1289
  return options.filterFn({
1081
1290
  ...operation,
1082
- path: path2,
1291
+ path,
1083
1292
  method
1084
1293
  });
1085
1294
  }
@@ -1088,22 +1297,22 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1088
1297
  /**
1089
1298
  * Generate a tool name
1090
1299
  */
1091
- generateToolName(path2, method, operationId, options = {}) {
1300
+ generateToolName(path, method, operationId, options = {}) {
1092
1301
  if (options.namingStrategy?.toolNameGenerator) {
1093
- return options.namingStrategy.toolNameGenerator(path2, method, operationId);
1302
+ return options.namingStrategy.toolNameGenerator(path, method, operationId);
1094
1303
  }
1095
1304
  if (operationId) {
1096
1305
  return operationId;
1097
1306
  }
1098
- const sanitized = path2.replace(/\{([^}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1307
+ const sanitized = path.replace(/\{([^}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1099
1308
  return `${method}_${sanitized}`;
1100
1309
  }
1101
1310
  /**
1102
1311
  * Extract metadata from operation
1103
1312
  */
1104
- extractMetadata(path2, method, operation, document, outputSchema) {
1313
+ extractMetadata(path, method, operation, document, outputSchema) {
1105
1314
  const metadata = {
1106
- path: path2,
1315
+ path,
1107
1316
  method,
1108
1317
  operationId: operation.operationId,
1109
1318
  operationSummary: operation.summary,
@@ -1504,6 +1713,7 @@ var SecurityResolver = class {
1504
1713
  if (requiresSignature) {
1505
1714
  resolved.requiresSignature = true;
1506
1715
  resolved.signatureInfo = {
1716
+ /* c8 ignore next -- signatureScheme is always set from security.scheme */
1507
1717
  scheme: signatureScheme || "unknown"
1508
1718
  };
1509
1719
  }
@@ -1549,6 +1759,7 @@ var SecurityResolver = class {
1549
1759
  return this.resolveBasicAuth(context);
1550
1760
  case "digest":
1551
1761
  return this.resolveDigestAuth(context);
1762
+ /* c8 ignore next -- hoba is part of the same fall-through as mutual/negotiate/vapid/scram */
1552
1763
  case "hoba":
1553
1764
  case "mutual":
1554
1765
  case "negotiate":
@@ -1581,16 +1792,18 @@ var SecurityResolver = class {
1581
1792
  resolveDigestAuth(context) {
1582
1793
  const digest = context.digest;
1583
1794
  if (!digest) return void 0;
1795
+ const quoted = (v) => String(v).replace(/[\r\n]/g, "").replace(/"/g, '\\"');
1796
+ const token = (v) => String(v).replace(/[\r\n",]/g, "");
1584
1797
  const parts = [
1585
- `username="${digest.username}"`,
1586
- digest.realm ? `realm="${digest.realm}"` : "",
1587
- digest.nonce ? `nonce="${digest.nonce}"` : "",
1588
- digest.uri ? `uri="${digest.uri}"` : "",
1589
- digest.response ? `response="${digest.response}"` : "",
1590
- digest.opaque ? `opaque="${digest.opaque}"` : "",
1591
- digest.qop ? `qop=${digest.qop}` : "",
1592
- digest.nc ? `nc=${digest.nc}` : "",
1593
- digest.cnonce ? `cnonce="${digest.cnonce}"` : ""
1798
+ `username="${quoted(digest.username)}"`,
1799
+ digest.realm ? `realm="${quoted(digest.realm)}"` : "",
1800
+ digest.nonce ? `nonce="${quoted(digest.nonce)}"` : "",
1801
+ digest.uri ? `uri="${quoted(digest.uri)}"` : "",
1802
+ digest.response ? `response="${quoted(digest.response)}"` : "",
1803
+ digest.opaque ? `opaque="${quoted(digest.opaque)}"` : "",
1804
+ digest.qop ? `qop=${token(digest.qop)}` : "",
1805
+ digest.nc ? `nc=${token(digest.nc)}` : "",
1806
+ digest.cnonce ? `cnonce="${quoted(digest.cnonce)}"` : ""
1594
1807
  ].filter(Boolean);
1595
1808
  return `Digest ${parts.join(", ")}`;
1596
1809
  }
@@ -1697,6 +1910,7 @@ function createSecurityContext(auth) {
1697
1910
  }
1698
1911
  // Annotate the CommonJS export names for ESM import in node:
1699
1912
  0 && (module.exports = {
1913
+ BUILTIN_FORMAT_RESOLVERS,
1700
1914
  GenerationError,
1701
1915
  LoadError,
1702
1916
  OpenAPIToolError,
@@ -1711,5 +1925,6 @@ function createSecurityContext(auth) {
1711
1925
  Validator,
1712
1926
  createSecurityContext,
1713
1927
  isReferenceObject,
1928
+ resolveSchemaFormats,
1714
1929
  toJsonSchema
1715
1930
  });
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "mcp-from-openapi",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Production-ready library for converting OpenAPI specifications into MCP tool definitions",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
7
+ "packageManager": "yarn@4.14.1",
7
8
  "keywords": [
8
9
  "mcp",
9
10
  "model-context-protocol",
@@ -27,7 +28,7 @@
27
28
  },
28
29
  "homepage": "https://github.com/agentfront/mcp-from-openapi#readme",
29
30
  "engines": {
30
- "node": ">=18.0.0"
31
+ "node": ">=20.0.0"
31
32
  },
32
33
  "type": "commonjs",
33
34
  "main": "./index.js",
@@ -48,7 +49,7 @@
48
49
  }
49
50
  },
50
51
  "dependencies": {
51
- "@apidevtools/json-schema-ref-parser": "^11.9.3",
52
+ "@apidevtools/json-schema-ref-parser": "^15.3.5",
52
53
  "openapi-types": "^12.1.3",
53
54
  "yaml": "^2.8.3"
54
55
  },
@@ -60,6 +61,7 @@
60
61
  "@swc/helpers": "^0.5.18",
61
62
  "@swc/jest": "~0.2.38",
62
63
  "@types/jest": "^29.5.0",
64
+ "@types/json-schema": "^7.0.15",
63
65
  "@types/node": "^24.0.0",
64
66
  "esbuild": "^0.27.2",
65
67
  "jest": "^29.7.0",
package/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { JSONSchema } from 'zod/v4/core';
2
2
  /** JSON Schema type from Zod v4 */
3
- type JsonSchema = JSONSchema.JSONSchema;
3
+ export type JsonSchema = JSONSchema.JSONSchema;
4
4
  import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
5
5
  /**
6
6
  * OpenAPI specification version 3.0.x or 3.1.x
@@ -457,7 +457,28 @@ export interface GenerateOptions {
457
457
  * @default false
458
458
  */
459
459
  includeSecurityInInput?: boolean;
460
+ /**
461
+ * Enable built-in format-to-schema resolution.
462
+ * Enriches schemas with concrete constraints (patterns, descriptions, min/max)
463
+ * based on OpenAPI format values (uuid, date-time, email, int32, etc.).
464
+ * @default false
465
+ */
466
+ resolveFormats?: boolean;
467
+ /**
468
+ * Custom format resolvers. Keys are format names, values are functions
469
+ * that receive the original schema and return an enriched schema.
470
+ *
471
+ * When used with `resolveFormats: true`, custom resolvers are merged with
472
+ * built-in resolvers (custom takes precedence for the same format).
473
+ * When used without `resolveFormats`, only custom resolvers are applied.
474
+ */
475
+ formatResolvers?: Record<string, FormatResolver>;
460
476
  }
477
+ /**
478
+ * A function that enriches a JSON Schema based on its format field.
479
+ * Receives the schema and returns a new schema with additional constraints.
480
+ */
481
+ export type FormatResolver = (schema: JsonSchema) => JsonSchema;
461
482
  /**
462
483
  * Naming strategy for resolving parameter conflicts
463
484
  */
@@ -530,4 +551,3 @@ export interface ValidationWarning {
530
551
  */
531
552
  code?: string;
532
553
  }
533
- export {};
package/CHANGELOG.md DELETED
@@ -1,83 +0,0 @@
1
- ## [2.2.0] - 2026-04-07
2
-
3
- ## [2.1.2] - 2025-12-27
4
-
5
- ### Changed
6
-
7
- - Added `publish-alpha` Nx target to enable alpha publishing flow using common script.
8
-
9
- ## [2.1.1] - 2025-12-24
10
-
11
- ### Fixed
12
-
13
- - Revised build setup to emit CJS and ESM bundles with corrected export map and sideEffects flag to improve tree shaking.
14
- - Synced package entry points and type outputs with the new build artifacts while preserving the public API surface.
15
-
16
- ## [2.1.0] - 2025-12-19
17
-
18
- ### Added
19
-
20
- - Introduced logger support within the OpenAPI adapter to improve observability.
21
- - Expanded security handling with automatic auth type routing and enhanced documentation.
22
-
23
- ### Changed
24
-
25
- - Refined security resolver examples and guidance for broader authentication scheme coverage.
26
-
27
- ## [2.0.0] - 2025-12-11
28
-
29
- ### Breaking
30
-
31
- - Migrated to Zod v4 (now a peer dependency) and aligned JSON schema types with Zod’s v4 JSONSchema
32
- - Renamed utility export `toJSONSchema7` to `toJsonSchema`, affecting consumers of the types/helpers
33
-
34
- ### Changed
35
-
36
- - Updated SWC/Jest config to inline ES2022 settings and modernized tooling versions
37
- - Adjusted exports formatting and minor parameter resolver refactors for consistency
38
-
39
- # Changelog
40
-
41
- ## [1.0.0] - 2025-11-21
42
-
43
- ### Features
44
-
45
- - Production-ready library for converting OpenAPI specifications into MCP tool definitions
46
- - OpenAPI 3.0+ and Swagger 2.0 support
47
- - Comprehensive operation parsing:
48
- - RESTful endpoint detection (GET, POST, PUT, PATCH, DELETE, etc.)
49
- - Path parameter extraction and validation
50
- - Query parameter handling
51
- - Request body schema conversion
52
- - Response schema parsing
53
- - Advanced OpenAPI features:
54
- - Reference resolution ($ref) across the entire specification
55
- - Security scheme detection and configuration
56
- - Parameter conflict resolution
57
- - Operation naming controls and customization
58
- - Request mapper generation:
59
- - Automatic parameter mapping from MCP tool inputs to HTTP requests
60
- - Type-safe parameter handling
61
- - Support for different parameter locations (path, query, header, cookie)
62
- - Request body transformation
63
- - Tool metadata generation:
64
- - Descriptive tool names from operation IDs
65
- - Documentation from OpenAPI descriptions
66
- - Schema validation rules
67
- - Multiple input formats:
68
- - JSON OpenAPI specifications
69
- - YAML OpenAPI specifications
70
- - Inline specification objects
71
- - URL references to remote specifications
72
- - Type-safe TypeScript implementation
73
- - Node.js 18+ compatibility
74
-
75
- ### Documentation
76
-
77
- - Complete API reference
78
- - OpenAPI conversion examples
79
- - Integration guides for MCP servers
80
- - Best practices for tool generation
81
-
82
- This is the first official release of `mcp-from-openapi`, extracted from the FrontMCP framework to be published as a
83
- standalone, reusable library for the community.