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/esm/index.mjs CHANGED
@@ -1,8 +1,5 @@
1
1
  // src/generator.ts
2
2
  import * as yaml from "yaml";
3
- import * as path from "path";
4
- import * as fs from "fs/promises";
5
- import $RefParser from "@apidevtools/json-schema-ref-parser";
6
3
 
7
4
  // src/types.ts
8
5
  function isReferenceObject(obj) {
@@ -537,12 +534,12 @@ var Validator = class {
537
534
  * Validate paths
538
535
  */
539
536
  validatePaths(paths, errors, warnings) {
540
- for (const [path2, pathItem] of Object.entries(paths)) {
537
+ for (const [path, pathItem] of Object.entries(paths)) {
541
538
  if (!pathItem) continue;
542
- if (!path2.startsWith("/")) {
539
+ if (!path.startsWith("/")) {
543
540
  errors.push({
544
- message: `Path must start with '/': ${path2}`,
545
- path: `/paths/${path2}`,
541
+ message: `Path must start with '/': ${path}`,
542
+ path: `/paths/${path}`,
546
543
  code: "INVALID_PATH_FORMAT"
547
544
  });
548
545
  }
@@ -552,13 +549,13 @@ var Validator = class {
552
549
  const operation = pathItem[method];
553
550
  if (operation) {
554
551
  hasOperations = true;
555
- this.validateOperation(operation, path2, method, errors, warnings);
552
+ this.validateOperation(operation, path, method, errors, warnings);
556
553
  }
557
554
  }
558
555
  if (!hasOperations && !pathItem.$ref) {
559
556
  warnings.push({
560
- message: `Path has no operations: ${path2}`,
561
- path: `/paths/${path2}`,
557
+ message: `Path has no operations: ${path}`,
558
+ path: `/paths/${path}`,
562
559
  code: "NO_OPERATIONS"
563
560
  });
564
561
  }
@@ -567,33 +564,33 @@ var Validator = class {
567
564
  /**
568
565
  * Validate an operation
569
566
  */
570
- validateOperation(operation, path2, method, errors, warnings) {
571
- const basePath = `/paths/${path2}/${method}`;
567
+ validateOperation(operation, path, method, errors, warnings) {
568
+ const basePath = `/paths/${path}/${method}`;
572
569
  if (!operation.operationId) {
573
570
  warnings.push({
574
- message: `Operation missing operationId: ${method.toUpperCase()} ${path2}`,
571
+ message: `Operation missing operationId: ${method.toUpperCase()} ${path}`,
575
572
  path: `${basePath}/operationId`,
576
573
  code: "NO_OPERATION_ID"
577
574
  });
578
575
  }
579
576
  if (!operation.responses || Object.keys(operation.responses).length === 0) {
580
577
  errors.push({
581
- message: `Operation missing responses: ${method.toUpperCase()} ${path2}`,
578
+ message: `Operation missing responses: ${method.toUpperCase()} ${path}`,
582
579
  path: `${basePath}/responses`,
583
580
  code: "NO_RESPONSES"
584
581
  });
585
582
  }
586
583
  if (operation.parameters) {
587
- this.validateParameters(operation.parameters, path2, method, errors, warnings);
584
+ this.validateParameters(operation.parameters, path, method, errors, warnings);
588
585
  }
589
- const pathParams = path2.match(/\{([^}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
586
+ const pathParams = path.match(/\{([^}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
590
587
  const definedPathParams = new Set(
591
588
  operation.parameters?.filter((p) => p.in === "path").map((p) => p.name) ?? []
592
589
  );
593
590
  for (const param of pathParams) {
594
591
  if (!definedPathParams.has(param)) {
595
592
  errors.push({
596
- message: `Path parameter '${param}' not defined in parameters: ${method.toUpperCase()} ${path2}`,
593
+ message: `Path parameter '${param}' not defined in parameters: ${method.toUpperCase()} ${path}`,
597
594
  path: `${basePath}/parameters`,
598
595
  code: "MISSING_PATH_PARAMETER"
599
596
  });
@@ -603,8 +600,8 @@ var Validator = class {
603
600
  /**
604
601
  * Validate parameters
605
602
  */
606
- validateParameters(parameters, path2, method, errors, warnings) {
607
- const basePath = `/paths/${path2}/${method}/parameters`;
603
+ validateParameters(parameters, path, method, errors, warnings) {
604
+ const basePath = `/paths/${path}/${method}/parameters`;
608
605
  for (let i = 0; i < parameters.length; i++) {
609
606
  const param = parameters[i];
610
607
  const paramPath = `${basePath}/${i}`;
@@ -686,6 +683,115 @@ var SchemaError = class extends OpenAPIToolError {
686
683
  }
687
684
  };
688
685
 
686
+ // src/format-resolver.ts
687
+ var BUILTIN_FORMAT_RESOLVERS = {
688
+ // String formats
689
+ uuid: (schema) => ({
690
+ ...schema,
691
+ 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}$",
692
+ description: schema.description || "UUID string (RFC 4122)"
693
+ }),
694
+ "date-time": (schema) => ({
695
+ ...schema,
696
+ description: schema.description || "ISO 8601 date-time (e.g., 2024-01-15T09:30:00Z)"
697
+ }),
698
+ date: (schema) => ({
699
+ ...schema,
700
+ pattern: schema.pattern ?? "^\\d{4}-\\d{2}-\\d{2}$",
701
+ description: schema.description || "ISO 8601 date (e.g., 2024-01-15)"
702
+ }),
703
+ time: (schema) => ({
704
+ ...schema,
705
+ pattern: schema.pattern ?? "^\\d{2}:\\d{2}:\\d{2}",
706
+ description: schema.description || "ISO 8601 time (e.g., 09:30:00)"
707
+ }),
708
+ email: (schema) => ({
709
+ ...schema,
710
+ description: schema.description || "Email address (RFC 5322)"
711
+ }),
712
+ uri: (schema) => ({
713
+ ...schema,
714
+ description: schema.description || "URI (RFC 3986)"
715
+ }),
716
+ "uri-reference": (schema) => ({
717
+ ...schema,
718
+ description: schema.description || "URI reference (RFC 3986)"
719
+ }),
720
+ hostname: (schema) => ({
721
+ ...schema,
722
+ description: schema.description || "Internet hostname (RFC 1123)"
723
+ }),
724
+ ipv4: (schema) => ({
725
+ ...schema,
726
+ pattern: schema.pattern ?? "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$",
727
+ description: schema.description || "IPv4 address"
728
+ }),
729
+ ipv6: (schema) => ({
730
+ ...schema,
731
+ description: schema.description || "IPv6 address (RFC 4291)"
732
+ }),
733
+ // Integer formats
734
+ int32: (schema) => ({
735
+ ...schema,
736
+ minimum: schema.minimum ?? -2147483648,
737
+ maximum: schema.maximum ?? 2147483647
738
+ }),
739
+ int64: (schema) => ({
740
+ ...schema,
741
+ minimum: schema.minimum ?? Number.MIN_SAFE_INTEGER,
742
+ maximum: schema.maximum ?? Number.MAX_SAFE_INTEGER
743
+ }),
744
+ // Binary/encoding formats
745
+ byte: (schema) => ({
746
+ ...schema,
747
+ pattern: schema.pattern ?? "^[A-Za-z0-9+/]*={0,2}$",
748
+ description: schema.description || "Base64-encoded string (RFC 4648)"
749
+ }),
750
+ binary: (schema) => ({
751
+ ...schema,
752
+ description: schema.description || "Binary data"
753
+ }),
754
+ // Sensitive data formats
755
+ password: (schema) => ({
756
+ ...schema,
757
+ description: schema.description || "Password (sensitive, UI should mask input)"
758
+ })
759
+ };
760
+ function resolveSchemaFormats(schema, resolvers) {
761
+ if (!schema || typeof schema !== "object") return schema;
762
+ let result = { ...schema };
763
+ const format = result["format"];
764
+ if (format && resolvers[format]) {
765
+ result = { ...resolvers[format](result) };
766
+ }
767
+ if (result["properties"] && typeof result["properties"] === "object") {
768
+ const props = {};
769
+ for (const [key, value] of Object.entries(result["properties"])) {
770
+ props[key] = resolveSchemaFormats(value, resolvers);
771
+ }
772
+ result["properties"] = props;
773
+ }
774
+ if (result["items"]) {
775
+ if (Array.isArray(result["items"])) {
776
+ result["items"] = result["items"].map((item) => resolveSchemaFormats(item, resolvers));
777
+ } else {
778
+ result["items"] = resolveSchemaFormats(result["items"], resolvers);
779
+ }
780
+ }
781
+ if (result["additionalProperties"] && typeof result["additionalProperties"] === "object") {
782
+ result["additionalProperties"] = resolveSchemaFormats(result["additionalProperties"], resolvers);
783
+ }
784
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
785
+ if (result[key] && Array.isArray(result[key])) {
786
+ result[key] = result[key].map((s) => resolveSchemaFormats(s, resolvers));
787
+ }
788
+ }
789
+ if (result["not"] && typeof result["not"] === "object") {
790
+ result["not"] = resolveSchemaFormats(result["not"], resolvers);
791
+ }
792
+ return result;
793
+ }
794
+
689
795
  // src/generator.ts
690
796
  var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
691
797
  document;
@@ -750,6 +856,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
750
856
  */
751
857
  static async fromFile(filePath, options = {}) {
752
858
  try {
859
+ const [path, fs] = await Promise.all([import("path"), import("fs/promises")]);
753
860
  const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
754
861
  const content = await fs.readFile(absolutePath, "utf-8");
755
862
  const ext = path.extname(filePath).toLowerCase();
@@ -840,6 +947,31 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
840
947
  /^\[fe80:/i
841
948
  // bracketed IPv6 link-local
842
949
  ];
950
+ /**
951
+ * Decode an IPv4-mapped IPv6 host (`::ffff:169.254.169.254` or its hex form
952
+ * `::ffff:a9fe:a9fe`, optionally bracketed) to its embedded dotted-quad IPv4,
953
+ * or `null` if the host isn't IPv4-mapped. `new URL().hostname` normalizes
954
+ * `[::ffff:169.254.169.254]` to `[::ffff:a9fe:a9fe]`, which the plain
955
+ * dotted-quad blocklist patterns miss — letting an attacker reach a private /
956
+ * metadata IPv4 through the v6 mapping. We re-check the decoded v4.
957
+ */
958
+ static mappedIPv4FromIPv6(hostname) {
959
+ let h = hostname;
960
+ if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
961
+ const lower = h.toLowerCase();
962
+ const marker = lower.lastIndexOf("::ffff:");
963
+ if (marker === -1) return null;
964
+ const tail = lower.slice(marker + "::ffff:".length);
965
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(tail)) return tail;
966
+ const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
967
+ if (hex) {
968
+ const hi = parseInt(hex[1], 16);
969
+ const lo = parseInt(hex[2], 16);
970
+ if (Number.isNaN(hi) || Number.isNaN(lo)) return null;
971
+ return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
972
+ }
973
+ return null;
974
+ }
843
975
  /**
844
976
  * Check whether a hostname is blocked (internal/private IP or explicit blocklist).
845
977
  */
@@ -850,11 +982,16 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
850
982
  if (refOpts.blockedHosts.includes(hostname)) {
851
983
  return true;
852
984
  }
853
- for (const pattern of _OpenAPIToolGenerator.BLOCKED_HOSTNAME_PATTERNS) {
854
- if (typeof pattern === "string") {
855
- if (hostname === pattern) return true;
856
- } else {
857
- if (pattern.test(hostname)) return true;
985
+ const candidates = [hostname];
986
+ const mapped = _OpenAPIToolGenerator.mappedIPv4FromIPv6(hostname);
987
+ if (mapped) candidates.push(mapped);
988
+ for (const candidate of candidates) {
989
+ for (const pattern of _OpenAPIToolGenerator.BLOCKED_HOSTNAME_PATTERNS) {
990
+ if (typeof pattern === "string") {
991
+ if (candidate === pattern) return true;
992
+ } else {
993
+ if (pattern.test(candidate)) return true;
994
+ }
858
995
  }
859
996
  }
860
997
  return false;
@@ -884,6 +1021,14 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
884
1021
  const hasHostAllowlist = refOpts.allowedHosts.length > 0;
885
1022
  const hostAllowSet = new Set(refOpts.allowedHosts);
886
1023
  resolveConfig["http"] = {
1024
+ // SECURITY: never auto-follow HTTP redirects when resolving external
1025
+ // `$ref`s. `canRead` (below) validates only the INITIAL URL; the
1026
+ // resolver's default redirect-following (up to 5 hops) re-fetches the
1027
+ // `Location` target WITHOUT re-invoking `canRead`, so an allowlisted host
1028
+ // could 302 → `http://169.254.169.254/...` and smuggle a blocked target
1029
+ // past the allowlist/blocklist. Setting `redirects: 0` refuses the first
1030
+ // redirect; legitimate refs resolve in one hop.
1031
+ redirects: 0,
887
1032
  canRead: (file) => {
888
1033
  try {
889
1034
  const parsed = new URL(file.url);
@@ -909,29 +1054,84 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
909
1054
  return { resolve: resolveConfig };
910
1055
  }
911
1056
  /**
912
- * Initialize the generator (dereference if needed)
1057
+ * Does the document contain any EXTERNAL `$ref` (a ref that is not a local
1058
+ * JSON-pointer beginning with `#`)? Only external refs require the full
1059
+ * `$RefParser` (file/http resolvers, which pull Node builtins). A document
1060
+ * with only internal refs can be dereferenced with the runtime-agnostic
1061
+ * resolver below — so it works on V8 isolates (Cloudflare Workers) too.
1062
+ */
1063
+ static hasExternalRefs(node, seen = /* @__PURE__ */ new Set()) {
1064
+ if (node === null || typeof node !== "object") return false;
1065
+ if (seen.has(node)) return false;
1066
+ seen.add(node);
1067
+ if (Array.isArray(node)) return node.some((n) => _OpenAPIToolGenerator.hasExternalRefs(n, seen));
1068
+ const ref = node.$ref;
1069
+ if (typeof ref === "string" && !ref.startsWith("#")) return true;
1070
+ return Object.values(node).some(
1071
+ (v) => _OpenAPIToolGenerator.hasExternalRefs(v, seen)
1072
+ );
1073
+ }
1074
+ /**
1075
+ * Dereference local (`#/...`) `$ref`s without `$RefParser` — pure, dependency-
1076
+ * free, runtime-agnostic. A pointer cache makes circular schemas resolve to a
1077
+ * shared reference instead of recursing forever (same contract as `$RefParser`).
1078
+ */
1079
+ static dereferenceInternal(root) {
1080
+ const cache = /* @__PURE__ */ new Map();
1081
+ const resolvePointer = (ptr) => {
1082
+ const parts = ptr.replace(/^#\/?/, "").split("/").filter((p) => p.length > 0).map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
1083
+ let cur = root;
1084
+ for (const p of parts) cur = cur?.[p];
1085
+ return cur;
1086
+ };
1087
+ const walk = (node) => {
1088
+ if (node === null || typeof node !== "object") return node;
1089
+ if (Array.isArray(node)) return node.map(walk);
1090
+ const ref = node.$ref;
1091
+ if (typeof ref === "string" && ref.startsWith("#")) {
1092
+ const cached = cache.get(ref);
1093
+ if (cached !== void 0) return cached;
1094
+ const placeholder = {};
1095
+ cache.set(ref, placeholder);
1096
+ const resolved = walk(resolvePointer(ref));
1097
+ if (resolved && typeof resolved === "object") Object.assign(placeholder, resolved);
1098
+ return placeholder;
1099
+ }
1100
+ const out = {};
1101
+ for (const [k, v] of Object.entries(node)) out[k] = walk(v);
1102
+ return out;
1103
+ };
1104
+ return walk(root);
1105
+ }
1106
+ /**
1107
+ * Initialize the generator (dereference if needed, then validate)
913
1108
  */
914
1109
  async initialize() {
1110
+ if (this.options.dereference && !this.dereferencedDocument) {
1111
+ const cloned = JSON.parse(JSON.stringify(this.document));
1112
+ if (!_OpenAPIToolGenerator.hasExternalRefs(cloned)) {
1113
+ this.dereferencedDocument = _OpenAPIToolGenerator.dereferenceInternal(cloned);
1114
+ } else {
1115
+ try {
1116
+ const { default: $RefParser } = await import("@apidevtools/json-schema-ref-parser");
1117
+ const refParserOptions = this.buildRefParserOptions();
1118
+ this.dereferencedDocument = await $RefParser.dereference(cloned, refParserOptions);
1119
+ } catch (error) {
1120
+ const errorMessage = error instanceof Error ? error.message : String(error);
1121
+ throw new ParseError(`Failed to dereference OpenAPI document: ${errorMessage}`, {
1122
+ originalError: error
1123
+ });
1124
+ }
1125
+ }
1126
+ }
915
1127
  if (this.options.validate) {
916
- const result = await this.validate();
1128
+ const validator = new Validator();
1129
+ const documentToValidate = this.dereferencedDocument ?? this.document;
1130
+ const result = await validator.validate(documentToValidate);
917
1131
  if (!result.valid) {
918
1132
  throw new ParseError("Invalid OpenAPI document", { errors: result.errors });
919
1133
  }
920
1134
  }
921
- if (this.options.dereference && !this.dereferencedDocument) {
922
- try {
923
- const refParserOptions = this.buildRefParserOptions();
924
- this.dereferencedDocument = await $RefParser.dereference(
925
- JSON.parse(JSON.stringify(this.document)),
926
- refParserOptions
927
- );
928
- } catch (error) {
929
- const errorMessage = error instanceof Error ? error.message : String(error);
930
- throw new ParseError(`Failed to dereference OpenAPI document: ${errorMessage}`, {
931
- originalError: error
932
- });
933
- }
934
- }
935
1135
  }
936
1136
  /**
937
1137
  * Generate all tools from the OpenAPI specification
@@ -1000,11 +1200,18 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1000
1200
  const name = this.generateToolName(pathStr, method, operation.operationId, options);
1001
1201
  const description = operation.summary || operation.description || `${method.toUpperCase()} ${pathStr}`;
1002
1202
  const metadata = this.extractMetadata(pathStr, method, operation, document, outputSchema);
1203
+ const formatResolvers = {
1204
+ ...options.resolveFormats ? BUILTIN_FORMAT_RESOLVERS : {},
1205
+ ...options.formatResolvers
1206
+ };
1207
+ const hasFormatResolvers = Object.keys(formatResolvers).length > 0;
1208
+ const resolvedInputSchema = hasFormatResolvers ? resolveSchemaFormats(inputSchema, formatResolvers) : inputSchema;
1209
+ const resolvedOutputSchema = hasFormatResolvers && outputSchema ? resolveSchemaFormats(outputSchema, formatResolvers) : outputSchema;
1003
1210
  return {
1004
1211
  name,
1005
1212
  description,
1006
- inputSchema,
1007
- outputSchema,
1213
+ inputSchema: resolvedInputSchema,
1214
+ outputSchema: resolvedOutputSchema,
1008
1215
  mapper,
1009
1216
  metadata
1010
1217
  };
@@ -1012,7 +1219,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1012
1219
  /**
1013
1220
  * Check if an operation should be included
1014
1221
  */
1015
- shouldIncludeOperation(operation, path2, method, options) {
1222
+ shouldIncludeOperation(operation, path, method, options) {
1016
1223
  if (operation.deprecated && !options.includeDeprecated) {
1017
1224
  return false;
1018
1225
  }
@@ -1029,7 +1236,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1029
1236
  if (options.filterFn) {
1030
1237
  return options.filterFn({
1031
1238
  ...operation,
1032
- path: path2,
1239
+ path,
1033
1240
  method
1034
1241
  });
1035
1242
  }
@@ -1038,22 +1245,22 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1038
1245
  /**
1039
1246
  * Generate a tool name
1040
1247
  */
1041
- generateToolName(path2, method, operationId, options = {}) {
1248
+ generateToolName(path, method, operationId, options = {}) {
1042
1249
  if (options.namingStrategy?.toolNameGenerator) {
1043
- return options.namingStrategy.toolNameGenerator(path2, method, operationId);
1250
+ return options.namingStrategy.toolNameGenerator(path, method, operationId);
1044
1251
  }
1045
1252
  if (operationId) {
1046
1253
  return operationId;
1047
1254
  }
1048
- const sanitized = path2.replace(/\{([^}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1255
+ const sanitized = path.replace(/\{([^}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1049
1256
  return `${method}_${sanitized}`;
1050
1257
  }
1051
1258
  /**
1052
1259
  * Extract metadata from operation
1053
1260
  */
1054
- extractMetadata(path2, method, operation, document, outputSchema) {
1261
+ extractMetadata(path, method, operation, document, outputSchema) {
1055
1262
  const metadata = {
1056
- path: path2,
1263
+ path,
1057
1264
  method,
1058
1265
  operationId: operation.operationId,
1059
1266
  operationSummary: operation.summary,
@@ -1454,6 +1661,7 @@ var SecurityResolver = class {
1454
1661
  if (requiresSignature) {
1455
1662
  resolved.requiresSignature = true;
1456
1663
  resolved.signatureInfo = {
1664
+ /* c8 ignore next -- signatureScheme is always set from security.scheme */
1457
1665
  scheme: signatureScheme || "unknown"
1458
1666
  };
1459
1667
  }
@@ -1499,6 +1707,7 @@ var SecurityResolver = class {
1499
1707
  return this.resolveBasicAuth(context);
1500
1708
  case "digest":
1501
1709
  return this.resolveDigestAuth(context);
1710
+ /* c8 ignore next -- hoba is part of the same fall-through as mutual/negotiate/vapid/scram */
1502
1711
  case "hoba":
1503
1712
  case "mutual":
1504
1713
  case "negotiate":
@@ -1531,16 +1740,18 @@ var SecurityResolver = class {
1531
1740
  resolveDigestAuth(context) {
1532
1741
  const digest = context.digest;
1533
1742
  if (!digest) return void 0;
1743
+ const quoted = (v) => String(v).replace(/[\r\n]/g, "").replace(/"/g, '\\"');
1744
+ const token = (v) => String(v).replace(/[\r\n",]/g, "");
1534
1745
  const parts = [
1535
- `username="${digest.username}"`,
1536
- digest.realm ? `realm="${digest.realm}"` : "",
1537
- digest.nonce ? `nonce="${digest.nonce}"` : "",
1538
- digest.uri ? `uri="${digest.uri}"` : "",
1539
- digest.response ? `response="${digest.response}"` : "",
1540
- digest.opaque ? `opaque="${digest.opaque}"` : "",
1541
- digest.qop ? `qop=${digest.qop}` : "",
1542
- digest.nc ? `nc=${digest.nc}` : "",
1543
- digest.cnonce ? `cnonce="${digest.cnonce}"` : ""
1746
+ `username="${quoted(digest.username)}"`,
1747
+ digest.realm ? `realm="${quoted(digest.realm)}"` : "",
1748
+ digest.nonce ? `nonce="${quoted(digest.nonce)}"` : "",
1749
+ digest.uri ? `uri="${quoted(digest.uri)}"` : "",
1750
+ digest.response ? `response="${quoted(digest.response)}"` : "",
1751
+ digest.opaque ? `opaque="${quoted(digest.opaque)}"` : "",
1752
+ digest.qop ? `qop=${token(digest.qop)}` : "",
1753
+ digest.nc ? `nc=${token(digest.nc)}` : "",
1754
+ digest.cnonce ? `cnonce="${quoted(digest.cnonce)}"` : ""
1544
1755
  ].filter(Boolean);
1545
1756
  return `Digest ${parts.join(", ")}`;
1546
1757
  }
@@ -1646,6 +1857,7 @@ function createSecurityContext(auth) {
1646
1857
  };
1647
1858
  }
1648
1859
  export {
1860
+ BUILTIN_FORMAT_RESOLVERS,
1649
1861
  GenerationError,
1650
1862
  LoadError,
1651
1863
  OpenAPIToolError,
@@ -1660,5 +1872,6 @@ export {
1660
1872
  Validator,
1661
1873
  createSecurityContext,
1662
1874
  isReferenceObject,
1875
+ resolveSchemaFormats,
1663
1876
  toJsonSchema
1664
1877
  };
package/esm/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": "module",
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",
@@ -0,0 +1,12 @@
1
+ import type { JsonSchema, FormatResolver } from './types';
2
+ /**
3
+ * Built-in format resolvers that enrich JSON Schema with concrete constraints.
4
+ * Each resolver only adds constraints if not already present on the schema.
5
+ */
6
+ export declare const BUILTIN_FORMAT_RESOLVERS: Record<string, FormatResolver>;
7
+ /**
8
+ * Recursively resolve format fields in a JSON Schema tree.
9
+ * For each schema node with a `format` field, the matching resolver
10
+ * is applied to enrich the schema with concrete constraints.
11
+ */
12
+ export declare function resolveSchemaFormats(schema: JsonSchema, resolvers: Record<string, FormatResolver>): JsonSchema;
package/generator.d.ts CHANGED
@@ -39,6 +39,15 @@ export declare class OpenAPIToolGenerator {
39
39
  * Covers RFC 1918/6598 private ranges, link-local, loopback, and cloud metadata endpoints.
40
40
  */
41
41
  private static readonly BLOCKED_HOSTNAME_PATTERNS;
42
+ /**
43
+ * Decode an IPv4-mapped IPv6 host (`::ffff:169.254.169.254` or its hex form
44
+ * `::ffff:a9fe:a9fe`, optionally bracketed) to its embedded dotted-quad IPv4,
45
+ * or `null` if the host isn't IPv4-mapped. `new URL().hostname` normalizes
46
+ * `[::ffff:169.254.169.254]` to `[::ffff:a9fe:a9fe]`, which the plain
47
+ * dotted-quad blocklist patterns miss — letting an attacker reach a private /
48
+ * metadata IPv4 through the v6 mapping. We re-check the decoded v4.
49
+ */
50
+ private static mappedIPv4FromIPv6;
42
51
  /**
43
52
  * Check whether a hostname is blocked (internal/private IP or explicit blocklist).
44
53
  */
@@ -49,7 +58,21 @@ export declare class OpenAPIToolGenerator {
49
58
  */
50
59
  private buildRefParserOptions;
51
60
  /**
52
- * Initialize the generator (dereference if needed)
61
+ * Does the document contain any EXTERNAL `$ref` (a ref that is not a local
62
+ * JSON-pointer beginning with `#`)? Only external refs require the full
63
+ * `$RefParser` (file/http resolvers, which pull Node builtins). A document
64
+ * with only internal refs can be dereferenced with the runtime-agnostic
65
+ * resolver below — so it works on V8 isolates (Cloudflare Workers) too.
66
+ */
67
+ private static hasExternalRefs;
68
+ /**
69
+ * Dereference local (`#/...`) `$ref`s without `$RefParser` — pure, dependency-
70
+ * free, runtime-agnostic. A pointer cache makes circular schemas resolve to a
71
+ * shared reference instead of recursing forever (same contract as `$RefParser`).
72
+ */
73
+ private static dereferenceInternal;
74
+ /**
75
+ * Initialize the generator (dereference if needed, then validate)
53
76
  */
54
77
  private initialize;
55
78
  /**
package/index.d.ts CHANGED
@@ -4,7 +4,8 @@ export { ParameterResolver } from './parameter-resolver';
4
4
  export { ResponseBuilder } from './response-builder';
5
5
  export { Validator } from './validator';
6
6
  export { SecurityResolver, createSecurityContext } from './security-resolver';
7
+ export { BUILTIN_FORMAT_RESOLVERS, resolveSchemaFormats } from './format-resolver';
7
8
  export { OpenAPIToolError, LoadError, ParseError, ValidationError, GenerationError, SchemaError } from './errors';
8
- export type { McpOpenAPITool, ParameterMapper, ToolMetadata, FrontMcpExtensionData, SerializationInfo, SecurityRequirement, SecurityParameterInfo, ServerInfo, RefResolutionOptions, LoadOptions, GenerateOptions, NamingStrategy, OperationWithContext, OpenAPIDocument, OpenAPIVersion, HTTPMethod, ParameterLocation, AuthType, OperationObject, ParameterObject, RequestBodyObject, ResponseObject, ResponsesObject, MediaTypeObject, HeaderObject, ExampleObject, PathItemObject, PathsObject, ServerObject, SecuritySchemeObject, ReferenceObject, TagObject, ExternalDocumentationObject, ServerVariableObject, EncodingObject, SecurityRequirementObject, SchemaObject, ValidationResult, ValidationErrorDetail, ValidationWarning, } from './types';
9
+ export type { McpOpenAPITool, ParameterMapper, ToolMetadata, FrontMcpExtensionData, SerializationInfo, SecurityRequirement, SecurityParameterInfo, ServerInfo, RefResolutionOptions, LoadOptions, GenerateOptions, FormatResolver, JsonSchema, NamingStrategy, OperationWithContext, OpenAPIDocument, OpenAPIVersion, HTTPMethod, ParameterLocation, AuthType, OperationObject, ParameterObject, RequestBodyObject, ResponseObject, ResponsesObject, MediaTypeObject, HeaderObject, ExampleObject, PathItemObject, PathsObject, ServerObject, SecuritySchemeObject, ReferenceObject, TagObject, ExternalDocumentationObject, ServerVariableObject, EncodingObject, SecurityRequirementObject, SchemaObject, ValidationResult, ValidationErrorDetail, ValidationWarning, } from './types';
9
10
  export type { SecurityContext, ResolvedSecurity, DigestAuthCredentials, ClientCertificate, AWSCredentials, SignatureData, } from './security-resolver';
10
11
  export { isReferenceObject, toJsonSchema } from './types';