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/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 [
|
|
537
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
541
538
|
if (!pathItem) continue;
|
|
542
|
-
if (!
|
|
539
|
+
if (!path.startsWith("/")) {
|
|
543
540
|
errors.push({
|
|
544
|
-
message: `Path must start with '/': ${
|
|
545
|
-
path: `/paths/${
|
|
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,
|
|
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: ${
|
|
561
|
-
path: `/paths/${
|
|
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,
|
|
571
|
-
const basePath = `/paths/${
|
|
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()} ${
|
|
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()} ${
|
|
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,
|
|
584
|
+
this.validateParameters(operation.parameters, path, method, errors, warnings);
|
|
588
585
|
}
|
|
589
|
-
const pathParams =
|
|
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()} ${
|
|
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,
|
|
607
|
-
const basePath = `/paths/${
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
1248
|
+
generateToolName(path, method, operationId, options = {}) {
|
|
1042
1249
|
if (options.namingStrategy?.toolNameGenerator) {
|
|
1043
|
-
return options.namingStrategy.toolNameGenerator(
|
|
1250
|
+
return options.namingStrategy.toolNameGenerator(path, method, operationId);
|
|
1044
1251
|
}
|
|
1045
1252
|
if (operationId) {
|
|
1046
1253
|
return operationId;
|
|
1047
1254
|
}
|
|
1048
|
-
const sanitized =
|
|
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(
|
|
1261
|
+
extractMetadata(path, method, operation, document, outputSchema) {
|
|
1055
1262
|
const metadata = {
|
|
1056
|
-
path
|
|
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.
|
|
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": "module",
|
|
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",
|
|
@@ -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
|
-
*
|
|
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';
|