mcp-from-openapi 2.1.2 → 2.3.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,10 +1,9 @@
1
- // libs/mcp-from-openapi/src/generator.ts
1
+ // src/generator.ts
2
2
  import * as yaml from "yaml";
3
- import * as fs from "fs/promises";
4
3
  import * as path from "path";
5
- import $RefParser from "@apidevtools/json-schema-ref-parser";
4
+ import * as fs from "fs/promises";
6
5
 
7
- // libs/mcp-from-openapi/src/types.ts
6
+ // src/types.ts
8
7
  function isReferenceObject(obj) {
9
8
  return obj && typeof obj === "object" && "$ref" in obj;
10
9
  }
@@ -70,7 +69,7 @@ function toJsonSchema(schema) {
70
69
  return result;
71
70
  }
72
71
 
73
- // libs/mcp-from-openapi/src/parameter-resolver.ts
72
+ // src/parameter-resolver.ts
74
73
  var ParameterResolver = class {
75
74
  namingStrategy;
76
75
  constructor(namingStrategy) {
@@ -335,7 +334,7 @@ var ParameterResolver = class {
335
334
  }
336
335
  };
337
336
 
338
- // libs/mcp-from-openapi/src/response-builder.ts
337
+ // src/response-builder.ts
339
338
  var ResponseBuilder = class {
340
339
  preferredStatusCodes;
341
340
  includeAllResponses;
@@ -455,7 +454,7 @@ var ResponseBuilder = class {
455
454
  }
456
455
  };
457
456
 
458
- // libs/mcp-from-openapi/src/validator.ts
457
+ // src/validator.ts
459
458
  var Validator = class {
460
459
  /**
461
460
  * Validate an OpenAPI document
@@ -646,7 +645,7 @@ var Validator = class {
646
645
  }
647
646
  };
648
647
 
649
- // libs/mcp-from-openapi/src/errors.ts
648
+ // src/errors.ts
650
649
  var OpenAPIToolError = class extends Error {
651
650
  context;
652
651
  constructor(message, context) {
@@ -686,7 +685,116 @@ var SchemaError = class extends OpenAPIToolError {
686
685
  }
687
686
  };
688
687
 
689
- // libs/mcp-from-openapi/src/generator.ts
688
+ // src/format-resolver.ts
689
+ var BUILTIN_FORMAT_RESOLVERS = {
690
+ // String formats
691
+ uuid: (schema) => ({
692
+ ...schema,
693
+ 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}$",
694
+ description: schema.description || "UUID string (RFC 4122)"
695
+ }),
696
+ "date-time": (schema) => ({
697
+ ...schema,
698
+ description: schema.description || "ISO 8601 date-time (e.g., 2024-01-15T09:30:00Z)"
699
+ }),
700
+ date: (schema) => ({
701
+ ...schema,
702
+ pattern: schema.pattern ?? "^\\d{4}-\\d{2}-\\d{2}$",
703
+ description: schema.description || "ISO 8601 date (e.g., 2024-01-15)"
704
+ }),
705
+ time: (schema) => ({
706
+ ...schema,
707
+ pattern: schema.pattern ?? "^\\d{2}:\\d{2}:\\d{2}",
708
+ description: schema.description || "ISO 8601 time (e.g., 09:30:00)"
709
+ }),
710
+ email: (schema) => ({
711
+ ...schema,
712
+ description: schema.description || "Email address (RFC 5322)"
713
+ }),
714
+ uri: (schema) => ({
715
+ ...schema,
716
+ description: schema.description || "URI (RFC 3986)"
717
+ }),
718
+ "uri-reference": (schema) => ({
719
+ ...schema,
720
+ description: schema.description || "URI reference (RFC 3986)"
721
+ }),
722
+ hostname: (schema) => ({
723
+ ...schema,
724
+ description: schema.description || "Internet hostname (RFC 1123)"
725
+ }),
726
+ ipv4: (schema) => ({
727
+ ...schema,
728
+ pattern: schema.pattern ?? "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$",
729
+ description: schema.description || "IPv4 address"
730
+ }),
731
+ ipv6: (schema) => ({
732
+ ...schema,
733
+ description: schema.description || "IPv6 address (RFC 4291)"
734
+ }),
735
+ // Integer formats
736
+ int32: (schema) => ({
737
+ ...schema,
738
+ minimum: schema.minimum ?? -2147483648,
739
+ maximum: schema.maximum ?? 2147483647
740
+ }),
741
+ int64: (schema) => ({
742
+ ...schema,
743
+ minimum: schema.minimum ?? Number.MIN_SAFE_INTEGER,
744
+ maximum: schema.maximum ?? Number.MAX_SAFE_INTEGER
745
+ }),
746
+ // Binary/encoding formats
747
+ byte: (schema) => ({
748
+ ...schema,
749
+ pattern: schema.pattern ?? "^[A-Za-z0-9+/]*={0,2}$",
750
+ description: schema.description || "Base64-encoded string (RFC 4648)"
751
+ }),
752
+ binary: (schema) => ({
753
+ ...schema,
754
+ description: schema.description || "Binary data"
755
+ }),
756
+ // Sensitive data formats
757
+ password: (schema) => ({
758
+ ...schema,
759
+ description: schema.description || "Password (sensitive, UI should mask input)"
760
+ })
761
+ };
762
+ function resolveSchemaFormats(schema, resolvers) {
763
+ if (!schema || typeof schema !== "object") return schema;
764
+ let result = { ...schema };
765
+ const format = result["format"];
766
+ if (format && resolvers[format]) {
767
+ result = { ...resolvers[format](result) };
768
+ }
769
+ if (result["properties"] && typeof result["properties"] === "object") {
770
+ const props = {};
771
+ for (const [key, value] of Object.entries(result["properties"])) {
772
+ props[key] = resolveSchemaFormats(value, resolvers);
773
+ }
774
+ result["properties"] = props;
775
+ }
776
+ if (result["items"]) {
777
+ if (Array.isArray(result["items"])) {
778
+ result["items"] = result["items"].map((item) => resolveSchemaFormats(item, resolvers));
779
+ } else {
780
+ result["items"] = resolveSchemaFormats(result["items"], resolvers);
781
+ }
782
+ }
783
+ if (result["additionalProperties"] && typeof result["additionalProperties"] === "object") {
784
+ result["additionalProperties"] = resolveSchemaFormats(result["additionalProperties"], resolvers);
785
+ }
786
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
787
+ if (result[key] && Array.isArray(result[key])) {
788
+ result[key] = result[key].map((s) => resolveSchemaFormats(s, resolvers));
789
+ }
790
+ }
791
+ if (result["not"] && typeof result["not"] === "object") {
792
+ result["not"] = resolveSchemaFormats(result["not"], resolvers);
793
+ }
794
+ return result;
795
+ }
796
+
797
+ // src/generator.ts
690
798
  var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
691
799
  document;
692
800
  dereferencedDocument;
@@ -702,7 +810,8 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
702
810
  headers: options.headers ?? {},
703
811
  timeout: options.timeout ?? 3e4,
704
812
  validate: options.validate ?? true,
705
- followRedirects: options.followRedirects ?? true
813
+ followRedirects: options.followRedirects ?? true,
814
+ refResolution: options.refResolution ?? {}
706
815
  };
707
816
  }
708
817
  /**
@@ -808,19 +917,116 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
808
917
  return validator.validate(this.document);
809
918
  }
810
919
  /**
811
- * Initialize the generator (dereference if needed)
920
+ * Hostnames and IP patterns that are blocked by default to prevent SSRF.
921
+ * Covers RFC 1918/6598 private ranges, link-local, loopback, and cloud metadata endpoints.
812
922
  */
813
- async initialize() {
814
- if (this.options.validate) {
815
- const result = await this.validate();
816
- if (!result.valid) {
817
- throw new ParseError("Invalid OpenAPI document", { errors: result.errors });
923
+ static BLOCKED_HOSTNAME_PATTERNS = [
924
+ "localhost",
925
+ "metadata.google.internal",
926
+ /^127\.\d+\.\d+\.\d+$/,
927
+ // 127.0.0.0/8 loopback
928
+ /^10\.\d+\.\d+\.\d+$/,
929
+ // 10.0.0.0/8 private
930
+ /^172\.(1[6-9]|2\d|3[01])\.\d+\.\d+$/,
931
+ // 172.16.0.0/12 private
932
+ /^192\.168\.\d+\.\d+$/,
933
+ // 192.168.0.0/16 private
934
+ /^169\.254\.\d+\.\d+$/,
935
+ // 169.254.0.0/16 link-local / cloud metadata
936
+ /^0\.0\.0\.0$/,
937
+ // unspecified
938
+ "::1",
939
+ // IPv6 loopback
940
+ /^fd[0-9a-f]{2}:/i,
941
+ // fd00::/8 IPv6 ULA
942
+ /^fe80:/i,
943
+ // fe80::/10 IPv6 link-local
944
+ /^\[::1\]$/,
945
+ // bracketed IPv6 loopback
946
+ /^\[fd[0-9a-f]{2}:/i,
947
+ // bracketed IPv6 ULA
948
+ /^\[fe80:/i
949
+ // bracketed IPv6 link-local
950
+ ];
951
+ /**
952
+ * Check whether a hostname is blocked (internal/private IP or explicit blocklist).
953
+ */
954
+ isBlockedHost(hostname, refOpts) {
955
+ if (refOpts.allowInternalIPs) {
956
+ return refOpts.blockedHosts.includes(hostname);
957
+ }
958
+ if (refOpts.blockedHosts.includes(hostname)) {
959
+ return true;
960
+ }
961
+ for (const pattern of _OpenAPIToolGenerator.BLOCKED_HOSTNAME_PATTERNS) {
962
+ if (typeof pattern === "string") {
963
+ if (hostname === pattern) return true;
964
+ } else {
965
+ if (pattern.test(hostname)) return true;
818
966
  }
819
967
  }
968
+ return false;
969
+ }
970
+ /**
971
+ * Build $RefParser options based on refResolution configuration.
972
+ * Defaults: allow http/https, block file://, block internal IPs.
973
+ */
974
+ buildRefParserOptions() {
975
+ const raw = this.options.refResolution;
976
+ const refOpts = {
977
+ allowedProtocols: raw.allowedProtocols ?? ["http", "https"],
978
+ allowedHosts: raw.allowedHosts ?? [],
979
+ blockedHosts: raw.blockedHosts ?? [],
980
+ allowInternalIPs: raw.allowInternalIPs ?? false
981
+ };
982
+ const allowedProtocols = new Set(refOpts.allowedProtocols);
983
+ const hasNetworkProtocol = allowedProtocols.size > 0 && !([...allowedProtocols].length === 1 && allowedProtocols.has("file"));
984
+ if (allowedProtocols.size === 0) {
985
+ return { resolve: { external: false } };
986
+ }
987
+ const resolveConfig = {
988
+ external: true,
989
+ file: allowedProtocols.has("file") ? void 0 : false
990
+ };
991
+ if (hasNetworkProtocol) {
992
+ const hasHostAllowlist = refOpts.allowedHosts.length > 0;
993
+ const hostAllowSet = new Set(refOpts.allowedHosts);
994
+ resolveConfig["http"] = {
995
+ canRead: (file) => {
996
+ try {
997
+ const parsed = new URL(file.url);
998
+ const protocol = parsed.protocol.replace(":", "");
999
+ if (!allowedProtocols.has(protocol)) {
1000
+ return false;
1001
+ }
1002
+ if (hasHostAllowlist && !hostAllowSet.has(parsed.hostname)) {
1003
+ return false;
1004
+ }
1005
+ if (this.isBlockedHost(parsed.hostname, refOpts)) {
1006
+ return false;
1007
+ }
1008
+ return true;
1009
+ } catch {
1010
+ return false;
1011
+ }
1012
+ }
1013
+ };
1014
+ } else {
1015
+ resolveConfig["http"] = false;
1016
+ }
1017
+ return { resolve: resolveConfig };
1018
+ }
1019
+ /**
1020
+ * Initialize the generator (dereference if needed, then validate)
1021
+ */
1022
+ async initialize() {
820
1023
  if (this.options.dereference && !this.dereferencedDocument) {
821
1024
  try {
1025
+ const { default: $RefParser } = await import("@apidevtools/json-schema-ref-parser");
1026
+ const refParserOptions = this.buildRefParserOptions();
822
1027
  this.dereferencedDocument = await $RefParser.dereference(
823
- JSON.parse(JSON.stringify(this.document))
1028
+ JSON.parse(JSON.stringify(this.document)),
1029
+ refParserOptions
824
1030
  );
825
1031
  } catch (error) {
826
1032
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -829,6 +1035,14 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
829
1035
  });
830
1036
  }
831
1037
  }
1038
+ if (this.options.validate) {
1039
+ const validator = new Validator();
1040
+ const documentToValidate = this.dereferencedDocument ?? this.document;
1041
+ const result = await validator.validate(documentToValidate);
1042
+ if (!result.valid) {
1043
+ throw new ParseError("Invalid OpenAPI document", { errors: result.errors });
1044
+ }
1045
+ }
832
1046
  }
833
1047
  /**
834
1048
  * Generate all tools from the OpenAPI specification
@@ -897,11 +1111,18 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
897
1111
  const name = this.generateToolName(pathStr, method, operation.operationId, options);
898
1112
  const description = operation.summary || operation.description || `${method.toUpperCase()} ${pathStr}`;
899
1113
  const metadata = this.extractMetadata(pathStr, method, operation, document, outputSchema);
1114
+ const formatResolvers = {
1115
+ ...options.resolveFormats ? BUILTIN_FORMAT_RESOLVERS : {},
1116
+ ...options.formatResolvers
1117
+ };
1118
+ const hasFormatResolvers = Object.keys(formatResolvers).length > 0;
1119
+ const resolvedInputSchema = hasFormatResolvers ? resolveSchemaFormats(inputSchema, formatResolvers) : inputSchema;
1120
+ const resolvedOutputSchema = hasFormatResolvers && outputSchema ? resolveSchemaFormats(outputSchema, formatResolvers) : outputSchema;
900
1121
  return {
901
1122
  name,
902
1123
  description,
903
- inputSchema,
904
- outputSchema,
1124
+ inputSchema: resolvedInputSchema,
1125
+ outputSchema: resolvedOutputSchema,
905
1126
  mapper,
906
1127
  metadata
907
1128
  };
@@ -1024,7 +1245,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1024
1245
  }
1025
1246
  };
1026
1247
 
1027
- // libs/mcp-from-openapi/src/schema-builder.ts
1248
+ // src/schema-builder.ts
1028
1249
  var SchemaBuilder = class {
1029
1250
  /**
1030
1251
  * Merge multiple schemas into one
@@ -1303,7 +1524,7 @@ var SchemaBuilder = class {
1303
1524
  }
1304
1525
  };
1305
1526
 
1306
- // libs/mcp-from-openapi/src/security-resolver.ts
1527
+ // src/security-resolver.ts
1307
1528
  var SecurityResolver = class {
1308
1529
  /**
1309
1530
  * Resolve security parameters from mapper entries
@@ -1351,6 +1572,7 @@ var SecurityResolver = class {
1351
1572
  if (requiresSignature) {
1352
1573
  resolved.requiresSignature = true;
1353
1574
  resolved.signatureInfo = {
1575
+ /* c8 ignore next -- signatureScheme is always set from security.scheme */
1354
1576
  scheme: signatureScheme || "unknown"
1355
1577
  };
1356
1578
  }
@@ -1396,6 +1618,7 @@ var SecurityResolver = class {
1396
1618
  return this.resolveBasicAuth(context);
1397
1619
  case "digest":
1398
1620
  return this.resolveDigestAuth(context);
1621
+ /* c8 ignore next -- hoba is part of the same fall-through as mutual/negotiate/vapid/scram */
1399
1622
  case "hoba":
1400
1623
  case "mutual":
1401
1624
  case "negotiate":
@@ -1543,6 +1766,7 @@ function createSecurityContext(auth) {
1543
1766
  };
1544
1767
  }
1545
1768
  export {
1769
+ BUILTIN_FORMAT_RESOLVERS,
1546
1770
  GenerationError,
1547
1771
  LoadError,
1548
1772
  OpenAPIToolError,
@@ -1557,5 +1781,6 @@ export {
1557
1781
  Validator,
1558
1782
  createSecurityContext,
1559
1783
  isReferenceObject,
1784
+ resolveSchemaFormats,
1560
1785
  toJsonSchema
1561
1786
  };
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-from-openapi",
3
- "version": "2.1.2",
3
+ "version": "2.3.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",
@@ -20,15 +20,14 @@
20
20
  ],
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "git+https://github.com/agentfront/frontmcp.git",
24
- "directory": "libs/mcp-from-openapi"
23
+ "url": "git+https://github.com/agentfront/mcp-from-openapi.git"
25
24
  },
26
25
  "bugs": {
27
- "url": "https://github.com/agentfront/frontmcp/issues"
26
+ "url": "https://github.com/agentfront/mcp-from-openapi/issues"
28
27
  },
29
- "homepage": "https://github.com/agentfront/frontmcp/blob/main/libs/mcp-from-openapi/README.md",
28
+ "homepage": "https://github.com/agentfront/mcp-from-openapi#readme",
30
29
  "engines": {
31
- "node": ">=18.0.0"
30
+ "node": ">=20.0.0"
32
31
  },
33
32
  "type": "module",
34
33
  "main": "../index.js",
@@ -46,19 +45,26 @@
46
45
  "types": "../index.d.ts",
47
46
  "default": "./index.mjs"
48
47
  }
49
- },
50
- "./esm": null
48
+ }
51
49
  },
52
50
  "dependencies": {
53
- "@apidevtools/json-schema-ref-parser": "^11.9.3",
51
+ "@apidevtools/json-schema-ref-parser": "^15.3.5",
54
52
  "openapi-types": "^12.1.3",
55
- "yaml": "^2.8.1"
53
+ "yaml": "^2.8.3"
56
54
  },
57
55
  "peerDependencies": {
58
56
  "zod": "^4.0.0"
59
57
  },
60
58
  "devDependencies": {
59
+ "@swc/core": "~1.5.7",
60
+ "@swc/helpers": "^0.5.18",
61
+ "@swc/jest": "~0.2.38",
62
+ "@types/jest": "^29.5.0",
63
+ "@types/json-schema": "^7.0.15",
61
64
  "@types/node": "^24.0.0",
65
+ "esbuild": "^0.27.2",
66
+ "jest": "^29.7.0",
67
+ "tslib": "^2.8.1",
62
68
  "typescript": "^5.0.0",
63
69
  "zod": "^4.0.0"
64
70
  }
@@ -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
@@ -35,7 +35,21 @@ export declare class OpenAPIToolGenerator {
35
35
  */
36
36
  validate(): Promise<ValidationResult>;
37
37
  /**
38
- * Initialize the generator (dereference if needed)
38
+ * Hostnames and IP patterns that are blocked by default to prevent SSRF.
39
+ * Covers RFC 1918/6598 private ranges, link-local, loopback, and cloud metadata endpoints.
40
+ */
41
+ private static readonly BLOCKED_HOSTNAME_PATTERNS;
42
+ /**
43
+ * Check whether a hostname is blocked (internal/private IP or explicit blocklist).
44
+ */
45
+ private isBlockedHost;
46
+ /**
47
+ * Build $RefParser options based on refResolution configuration.
48
+ * Defaults: allow http/https, block file://, block internal IPs.
49
+ */
50
+ private buildRefParserOptions;
51
+ /**
52
+ * Initialize the generator (dereference if needed, then validate)
39
53
  */
40
54
  private initialize;
41
55
  /**
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, 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';