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/README.md +61 -554
- package/esm/index.mjs +271 -58
- package/esm/package.json +5 -3
- package/format-resolver.d.ts +12 -0
- package/generator.d.ts +24 -1
- package/index.d.ts +2 -1
- package/index.js +273 -58
- package/package.json +5 -3
- package/types.d.ts +22 -2
- package/CHANGELOG.md +0 -83
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 [
|
|
589
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
591
590
|
if (!pathItem) continue;
|
|
592
|
-
if (!
|
|
591
|
+
if (!path.startsWith("/")) {
|
|
593
592
|
errors.push({
|
|
594
|
-
message: `Path must start with '/': ${
|
|
595
|
-
path: `/paths/${
|
|
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,
|
|
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: ${
|
|
611
|
-
path: `/paths/${
|
|
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,
|
|
621
|
-
const basePath = `/paths/${
|
|
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()} ${
|
|
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()} ${
|
|
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,
|
|
636
|
+
this.validateParameters(operation.parameters, path, method, errors, warnings);
|
|
638
637
|
}
|
|
639
|
-
const pathParams =
|
|
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()} ${
|
|
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,
|
|
657
|
-
const basePath = `/paths/${
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
1300
|
+
generateToolName(path, method, operationId, options = {}) {
|
|
1092
1301
|
if (options.namingStrategy?.toolNameGenerator) {
|
|
1093
|
-
return options.namingStrategy.toolNameGenerator(
|
|
1302
|
+
return options.namingStrategy.toolNameGenerator(path, method, operationId);
|
|
1094
1303
|
}
|
|
1095
1304
|
if (operationId) {
|
|
1096
1305
|
return operationId;
|
|
1097
1306
|
}
|
|
1098
|
-
const sanitized =
|
|
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(
|
|
1313
|
+
extractMetadata(path, method, operation, document, outputSchema) {
|
|
1105
1314
|
const metadata = {
|
|
1106
|
-
path
|
|
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.
|
|
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": ">=
|
|
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": "^
|
|
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.
|