mcp-from-openapi 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  OpenAPIToolGenerator: () => OpenAPIToolGenerator,
39
39
  ParameterResolver: () => ParameterResolver,
40
40
  ParseError: () => ParseError,
41
+ RequestBuildError: () => RequestBuildError,
41
42
  ResponseBuilder: () => ResponseBuilder,
42
43
  SchemaBuilder: () => SchemaBuilder,
43
44
  SchemaError: () => SchemaError,
@@ -45,17 +46,30 @@ __export(index_exports, {
45
46
  SsrfError: () => SsrfError,
46
47
  ValidationError: () => ValidationError,
47
48
  Validator: () => Validator,
49
+ applyClientTarget: () => applyClientTarget,
48
50
  assertUrlSafe: () => assertUrlSafe,
51
+ buildHttpRequest: () => buildHttpRequest,
52
+ collapseNestedUnions: () => collapseNestedUnions,
53
+ collapseRootCompositions: () => collapseRootCompositions,
49
54
  createSecurityContext: () => createSecurityContext,
50
55
  decodeIpv4MappedIpv6: () => decodeIpv4MappedIpv6,
51
56
  defaultLookup: () => defaultLookup,
57
+ demoteFormats: () => demoteFormats,
58
+ enforceClosedObjects: () => enforceClosedObjects,
59
+ ensureArrayItems: () => ensureArrayItems,
60
+ extractExtensionOverrides: () => extractExtensionOverrides,
61
+ inferAnnotationsFromMethod: () => inferAnnotationsFromMethod,
62
+ inlineLocalRefs: () => inlineLocalRefs,
52
63
  isBlockedAddress: () => isBlockedAddress,
53
64
  isBlockedHostname: () => isBlockedHostname,
54
65
  isReferenceObject: () => isReferenceObject,
55
66
  normalizeSsrfOptions: () => normalizeSsrfOptions,
67
+ requireAllProperties: () => requireAllProperties,
68
+ resolveExtensionEnabled: () => resolveExtensionEnabled,
56
69
  resolveSchemaFormats: () => resolveSchemaFormats,
57
70
  safeFetch: () => safeFetch,
58
- toJsonSchema: () => toJsonSchema
71
+ toJsonSchema: () => toJsonSchema,
72
+ toSdkTool: () => toSdkTool
59
73
  });
60
74
  module.exports = __toCommonJS(index_exports);
61
75
 
@@ -71,7 +85,25 @@ function toJsonSchema(schema) {
71
85
  return { $ref: schema.$ref };
72
86
  }
73
87
  const { exclusiveMaximum, exclusiveMinimum, maximum, minimum, ...rest } = schema;
74
- const result = { ...rest };
88
+ const { nullable, example, ...cleanRest } = rest;
89
+ const result = { ...cleanRest };
90
+ delete result["xml"];
91
+ let wrapNullable = false;
92
+ if (nullable === true) {
93
+ const type = result["type"];
94
+ if (type === void 0) {
95
+ wrapNullable = true;
96
+ } else if (Array.isArray(type)) {
97
+ if (!type.includes("null")) {
98
+ result["type"] = [...type, "null"];
99
+ }
100
+ } else if (type !== "null") {
101
+ result["type"] = [type, "null"];
102
+ }
103
+ }
104
+ if (example !== void 0 && !Array.isArray(result["examples"])) {
105
+ result["examples"] = [example];
106
+ }
75
107
  if (typeof exclusiveMaximum === "boolean") {
76
108
  if (exclusiveMaximum && maximum !== void 0) {
77
109
  result["exclusiveMaximum"] = maximum;
@@ -125,16 +157,57 @@ function toJsonSchema(schema) {
125
157
  if (result["not"]) {
126
158
  result["not"] = toJsonSchema(result["not"]);
127
159
  }
160
+ for (const key of ["patternProperties", "$defs", "definitions", "dependentSchemas"]) {
161
+ const value = result[key];
162
+ if (value && typeof value === "object" && !Array.isArray(value)) {
163
+ const mapped = {};
164
+ for (const [name, sub] of Object.entries(value)) {
165
+ mapped[name] = toJsonSchema(sub);
166
+ }
167
+ result[key] = mapped;
168
+ }
169
+ }
170
+ for (const key of [
171
+ "contains",
172
+ "propertyNames",
173
+ "if",
174
+ "then",
175
+ "else",
176
+ "contentSchema",
177
+ "unevaluatedItems",
178
+ "unevaluatedProperties"
179
+ ]) {
180
+ const value = result[key];
181
+ if (value && typeof value === "object") {
182
+ result[key] = toJsonSchema(value);
183
+ }
184
+ }
185
+ if (Array.isArray(result["prefixItems"])) {
186
+ result["prefixItems"] = result["prefixItems"].map(toJsonSchema);
187
+ }
188
+ if (wrapNullable) {
189
+ const wrapper = {};
190
+ for (const key of ["title", "description", "deprecated", "examples"]) {
191
+ if (result[key] !== void 0) {
192
+ wrapper[key] = result[key];
193
+ delete result[key];
194
+ }
195
+ }
196
+ wrapper["anyOf"] = [result, { type: "null" }];
197
+ return wrapper;
198
+ }
128
199
  return result;
129
200
  }
130
201
 
131
202
  // src/parameter-resolver.ts
132
203
  var ParameterResolver = class {
133
204
  namingStrategy;
134
- constructor(namingStrategy) {
205
+ includeExamples;
206
+ constructor(namingStrategy, options) {
135
207
  this.namingStrategy = namingStrategy ?? {
136
208
  conflictResolver: this.defaultConflictResolver
137
209
  };
210
+ this.includeExamples = options?.includeExamples ?? false;
138
211
  }
139
212
  /**
140
213
  * Default conflict resolver: prefix with location
@@ -166,7 +239,8 @@ var ParameterResolver = class {
166
239
  style: param.style,
167
240
  explode: param.explode,
168
241
  allowReserved: param.allowReserved,
169
- deprecated: param.deprecated
242
+ deprecated: param.deprecated,
243
+ examples: this.includeExamples ? collectExampleValues(param.example, param.examples) : void 0
170
244
  };
171
245
  if (!parametersByName.has(param.name)) {
172
246
  parametersByName.set(param.name, []);
@@ -177,7 +251,8 @@ var ParameterResolver = class {
177
251
  const contentType = this.selectContentType(requestBody.content);
178
252
  const mediaType = requestBody.content[contentType];
179
253
  if (mediaType?.schema) {
180
- this.extractBodyParameters(mediaType.schema, parametersByName, requestBody.required ?? false, contentType);
254
+ const mediaExamples = this.includeExamples ? collectExampleValues(mediaType.example, mediaType.examples) : void 0;
255
+ this.extractBodyParameters(mediaType.schema, parametersByName, requestBody.required ?? false, contentType, mediaExamples, mediaType.encoding);
181
256
  }
182
257
  }
183
258
  const properties = {};
@@ -198,7 +273,9 @@ var ParameterResolver = class {
198
273
  required: param.required,
199
274
  style: param.style,
200
275
  explode: param.explode,
201
- serialization: param.serialization
276
+ allowReserved: param.allowReserved,
277
+ serialization: param.serialization,
278
+ ...param.wholeBody && { wholeBody: true }
202
279
  });
203
280
  } else {
204
281
  params.forEach((param, index) => {
@@ -214,7 +291,9 @@ var ParameterResolver = class {
214
291
  required: param.required,
215
292
  style: param.style,
216
293
  explode: param.explode,
217
- serialization: param.serialization
294
+ allowReserved: param.allowReserved,
295
+ serialization: param.serialization,
296
+ ...param.wholeBody && { wholeBody: true }
218
297
  });
219
298
  });
220
299
  }
@@ -239,23 +318,31 @@ var ParameterResolver = class {
239
318
  /**
240
319
  * Extract parameters from request body schema
241
320
  */
242
- extractBodyParameters(schema, parametersByName, required, contentType, prefix = "") {
321
+ extractBodyParameters(schema, parametersByName, required, contentType, mediaExamples, encoding, prefix = "") {
243
322
  if (!schema || typeof schema !== "object") return;
244
323
  const jsonSchema = toJsonSchema(schema);
245
- if (jsonSchema.type === "object" && jsonSchema.properties) {
246
- const requiredFields = new Set(jsonSchema.required ?? []);
247
- for (const [propName, propSchema] of Object.entries(jsonSchema.properties)) {
324
+ const flattened = flattenObjectBody(jsonSchema);
325
+ if (flattened) {
326
+ const requiredFields = flattened.required;
327
+ for (const [propName, propSchema] of Object.entries(flattened.properties)) {
248
328
  const fullName = prefix ? `${prefix}.${propName}` : propName;
249
329
  const isRequired = required && requiredFields.has(propName);
250
330
  if (typeof propSchema === "object") {
331
+ const propEncoding = encoding?.[propName];
332
+ const propExamples = mediaExamples?.map(
333
+ (ex) => ex !== null && typeof ex === "object" && !Array.isArray(ex) ? ex[propName] : void 0
334
+ ).filter((value) => value !== void 0);
251
335
  const info = {
252
336
  name: fullName,
253
337
  location: "body",
254
338
  required: isRequired,
255
339
  schema: propSchema,
256
340
  description: propSchema.description,
341
+ examples: propExamples && propExamples.length > 0 ? propExamples : void 0,
257
342
  serialization: {
258
- contentType
343
+ contentType,
344
+ ...propEncoding && { encoding: { [propName]: propEncoding } },
345
+ ...isBinarySchema(propSchema) && { binary: true }
259
346
  }
260
347
  };
261
348
  if (!parametersByName.has(fullName)) {
@@ -270,9 +357,13 @@ var ParameterResolver = class {
270
357
  name: bodyParamName,
271
358
  location: "body",
272
359
  required,
273
- schema,
360
+ schema: jsonSchema,
361
+ examples: mediaExamples,
362
+ wholeBody: true,
274
363
  serialization: {
275
- contentType
364
+ contentType,
365
+ ...encoding && Object.keys(encoding).length > 0 && { encoding },
366
+ ...isBinarySchema(jsonSchema) && { binary: true }
276
367
  }
277
368
  };
278
369
  if (!parametersByName.has(bodyParamName)) {
@@ -289,6 +380,9 @@ var ParameterResolver = class {
289
380
  if (param.description) {
290
381
  schema.description = param.description;
291
382
  }
383
+ if (param.examples && param.examples.length > 0) {
384
+ schema.examples = param.examples;
385
+ }
292
386
  if (param.deprecated) {
293
387
  schema["deprecated"] = true;
294
388
  }
@@ -385,21 +479,68 @@ var ParameterResolver = class {
385
479
  required: true,
386
480
  security: securityInfo
387
481
  });
388
- if (includeInInput) {
482
+ const schemeInInput = includeInInput === true || Array.isArray(includeInInput) && includeInInput.includes(scheme);
483
+ if (schemeInInput) {
389
484
  properties[inputKey] = schema;
390
485
  required.push(inputKey);
391
486
  }
392
487
  }
393
488
  }
394
489
  };
490
+ function collectObjectMembers(schema) {
491
+ if (!schema || typeof schema !== "object") return { properties: {}, required: /* @__PURE__ */ new Set() };
492
+ if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) return "union";
493
+ const properties = {};
494
+ const required = /* @__PURE__ */ new Set();
495
+ if (Array.isArray(schema.allOf)) {
496
+ for (const member of schema.allOf) {
497
+ const collected = collectObjectMembers(member);
498
+ if (collected === "union") return "union";
499
+ Object.assign(properties, collected.properties);
500
+ collected.required.forEach((field) => required.add(field));
501
+ }
502
+ }
503
+ if (schema.properties && typeof schema.properties === "object") {
504
+ Object.assign(properties, schema.properties);
505
+ }
506
+ if (Array.isArray(schema.required)) {
507
+ schema.required.forEach((field) => required.add(field));
508
+ }
509
+ return { properties, required };
510
+ }
511
+ function flattenObjectBody(schema) {
512
+ const collected = collectObjectMembers(schema);
513
+ if (collected === "union") return void 0;
514
+ return Object.keys(collected.properties).length > 0 ? collected : void 0;
515
+ }
516
+ function isBinarySchema(schema) {
517
+ if (!schema || typeof schema !== "object") return false;
518
+ const record = schema;
519
+ if (record["format"] === "binary") return true;
520
+ return typeof record["contentMediaType"] === "string" && record["contentEncoding"] === void 0 && record["type"] === void 0;
521
+ }
522
+ function collectExampleValues(example, examples) {
523
+ if (examples && !Array.isArray(examples)) {
524
+ const values = Object.values(examples).filter((entry) => entry !== null && typeof entry === "object" && !isReferenceObject(entry)).map((entry) => entry.value).filter((value) => value !== void 0);
525
+ if (values.length > 0) {
526
+ return values;
527
+ }
528
+ }
529
+ if (example !== void 0) {
530
+ return [example];
531
+ }
532
+ return void 0;
533
+ }
395
534
 
396
535
  // src/response-builder.ts
397
536
  var ResponseBuilder = class {
398
537
  preferredStatusCodes;
399
538
  includeAllResponses;
539
+ includeExamples;
400
540
  constructor(options = {}) {
401
541
  this.preferredStatusCodes = options.preferredStatusCodes ?? [200, 201, 204, 202, 203, 206];
402
542
  this.includeAllResponses = options.includeAllResponses ?? true;
543
+ this.includeExamples = options.includeExamples ?? false;
403
544
  }
404
545
  /**
405
546
  * Build output schema from responses
@@ -477,6 +618,12 @@ var ResponseBuilder = class {
477
618
  if (!schema.description && response.description) {
478
619
  schema.description = response.description;
479
620
  }
621
+ if (this.includeExamples) {
622
+ const mediaExamples = collectExampleValues(mediaType.example, mediaType.examples);
623
+ if (mediaExamples) {
624
+ schema.examples = mediaExamples;
625
+ }
626
+ }
480
627
  schema["x-content-type"] = contentType;
481
628
  return { statusCode, schema };
482
629
  }
@@ -513,1285 +660,2040 @@ var ResponseBuilder = class {
513
660
  }
514
661
  };
515
662
 
516
- // src/validator.ts
517
- var Validator = class {
663
+ // src/schema-builder.ts
664
+ var SchemaBuilder = class {
518
665
  /**
519
- * Validate an OpenAPI document
666
+ * Merge multiple schemas into one
520
667
  */
521
- async validate(document) {
522
- const errors = [];
523
- const warnings = [];
524
- if (!document.openapi) {
525
- errors.push({
526
- message: "Missing required field: openapi",
527
- path: "/openapi",
528
- code: "MISSING_OPENAPI_VERSION"
529
- });
530
- } else if (!this.isValidOpenAPIVersion(document.openapi)) {
531
- errors.push({
532
- message: `Unsupported OpenAPI version: ${document.openapi}. Expected 3.0.x or 3.1.x`,
533
- path: "/openapi",
534
- code: "INVALID_OPENAPI_VERSION"
535
- });
668
+ static merge(schemas) {
669
+ if (schemas.length === 0) {
670
+ return { type: "object" };
536
671
  }
537
- if (!document.info) {
538
- errors.push({
539
- message: "Missing required field: info",
540
- path: "/info",
541
- code: "MISSING_INFO"
542
- });
543
- } else {
544
- if (!document.info.title) {
545
- errors.push({
546
- message: "Missing required field: info.title",
547
- path: "/info/title",
548
- code: "MISSING_TITLE"
549
- });
672
+ if (schemas.length === 1) {
673
+ return schemas[0];
674
+ }
675
+ const merged = {
676
+ type: "object",
677
+ properties: {},
678
+ required: []
679
+ };
680
+ const allRequired = /* @__PURE__ */ new Set();
681
+ for (const schema of schemas) {
682
+ if (schema.properties) {
683
+ merged.properties = {
684
+ ...merged.properties,
685
+ ...schema.properties
686
+ };
550
687
  }
551
- if (!document.info.version) {
552
- errors.push({
553
- message: "Missing required field: info.version",
554
- path: "/info/version",
555
- code: "MISSING_VERSION"
556
- });
688
+ if (schema.required) {
689
+ schema.required.forEach((field) => allRequired.add(field));
557
690
  }
558
691
  }
559
- if (!document.paths || Object.keys(document.paths).length === 0) {
560
- warnings.push({
561
- message: "No paths defined in OpenAPI document",
562
- path: "/paths",
563
- code: "NO_PATHS"
564
- });
565
- } else {
566
- this.validatePaths(document.paths, errors, warnings);
692
+ if (allRequired.size > 0) {
693
+ merged.required = Array.from(allRequired);
567
694
  }
568
- if (!document.servers || document.servers.length === 0) {
569
- warnings.push({
570
- message: "No servers defined. You may need to provide a baseUrl option.",
571
- path: "/servers",
572
- code: "NO_SERVERS"
573
- });
695
+ return merged;
696
+ }
697
+ /**
698
+ * Create a union schema (oneOf)
699
+ */
700
+ static union(schemas) {
701
+ if (schemas.length === 0) {
702
+ return {};
574
703
  }
575
- if (document.security && !document.components?.securitySchemes) {
576
- warnings.push({
577
- message: "Security requirements defined but no security schemes found",
578
- path: "/security",
579
- code: "NO_SECURITY_SCHEMES"
580
- });
704
+ if (schemas.length === 1) {
705
+ return schemas[0];
581
706
  }
582
707
  return {
583
- valid: errors.length === 0,
584
- errors: errors.length > 0 ? errors : void 0,
585
- warnings: warnings.length > 0 ? warnings : void 0
708
+ oneOf: schemas
586
709
  };
587
710
  }
588
711
  /**
589
- * Check if OpenAPI version is valid
712
+ * Deep clone a schema
590
713
  */
591
- isValidOpenAPIVersion(version) {
592
- return /^3\.[01]\.\d+$/.test(version);
714
+ static clone(schema) {
715
+ return JSON.parse(JSON.stringify(schema));
593
716
  }
594
717
  /**
595
- * Validate paths
718
+ * Remove $ref from schema (assumes already dereferenced)
596
719
  */
597
- validatePaths(paths, errors, warnings) {
598
- for (const [path, pathItem] of Object.entries(paths)) {
599
- if (!pathItem) continue;
600
- if (!path.startsWith("/")) {
601
- errors.push({
602
- message: `Path must start with '/': ${path}`,
603
- path: `/paths/${path}`,
604
- code: "INVALID_PATH_FORMAT"
605
- });
606
- }
607
- const methods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
608
- let hasOperations = false;
609
- for (const method of methods) {
610
- const operation = pathItem[method];
611
- if (operation) {
612
- hasOperations = true;
613
- this.validateOperation(operation, path, method, errors, warnings);
720
+ static removeRefs(schema) {
721
+ const cloned = this.clone(schema);
722
+ this.removeRefsRecursive(cloned);
723
+ return cloned;
724
+ }
725
+ static removeRefsRecursive(obj) {
726
+ if (!obj || typeof obj !== "object") return;
727
+ if (obj.$ref) {
728
+ delete obj.$ref;
729
+ }
730
+ for (const key in obj) {
731
+ if (key in obj) {
732
+ const value = obj[key];
733
+ if (value && typeof value === "object") {
734
+ this.removeRefsRecursive(value);
614
735
  }
615
736
  }
616
- if (!hasOperations && !pathItem.$ref) {
617
- warnings.push({
618
- message: `Path has no operations: ${path}`,
619
- path: `/paths/${path}`,
620
- code: "NO_OPERATIONS"
621
- });
622
- }
623
737
  }
624
738
  }
625
739
  /**
626
- * Validate an operation
740
+ * Add description to schema
627
741
  */
628
- validateOperation(operation, path, method, errors, warnings) {
629
- const basePath = `/paths/${path}/${method}`;
630
- if (!operation.operationId) {
631
- warnings.push({
632
- message: `Operation missing operationId: ${method.toUpperCase()} ${path}`,
633
- path: `${basePath}/operationId`,
634
- code: "NO_OPERATION_ID"
635
- });
636
- }
637
- if (!operation.responses || Object.keys(operation.responses).length === 0) {
638
- errors.push({
639
- message: `Operation missing responses: ${method.toUpperCase()} ${path}`,
640
- path: `${basePath}/responses`,
641
- code: "NO_RESPONSES"
642
- });
643
- }
644
- if (operation.parameters) {
645
- this.validateParameters(operation.parameters, path, method, errors, warnings);
646
- }
647
- const pathParams = path.match(/\{([^}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
648
- const definedPathParams = new Set(
649
- operation.parameters?.filter((p) => p.in === "path").map((p) => p.name) ?? []
650
- );
651
- for (const param of pathParams) {
652
- if (!definedPathParams.has(param)) {
653
- errors.push({
654
- message: `Path parameter '${param}' not defined in parameters: ${method.toUpperCase()} ${path}`,
655
- path: `${basePath}/parameters`,
656
- code: "MISSING_PATH_PARAMETER"
657
- });
658
- }
659
- }
742
+ static withDescription(schema, description) {
743
+ return {
744
+ ...schema,
745
+ description
746
+ };
660
747
  }
661
748
  /**
662
- * Validate parameters
749
+ * Add example to schema
663
750
  */
664
- validateParameters(parameters, path, method, errors, warnings) {
665
- const basePath = `/paths/${path}/${method}/parameters`;
666
- for (let i = 0; i < parameters.length; i++) {
667
- const param = parameters[i];
668
- const paramPath = `${basePath}/${i}`;
669
- if (!param.name) {
670
- errors.push({
671
- message: "Parameter missing name",
672
- path: `${paramPath}/name`,
673
- code: "MISSING_PARAMETER_NAME"
674
- });
675
- }
676
- if (!param.in) {
677
- errors.push({
678
- message: 'Parameter missing "in" field',
679
- path: `${paramPath}/in`,
680
- code: "MISSING_PARAMETER_IN"
681
- });
682
- } else if (!["path", "query", "header", "cookie"].includes(param.in)) {
683
- errors.push({
684
- message: `Invalid parameter location: ${param.in}`,
685
- path: `${paramPath}/in`,
686
- code: "INVALID_PARAMETER_IN"
687
- });
688
- }
689
- if (param.in === "path" && !param.required) {
690
- errors.push({
691
- message: `Path parameter '${param.name}' must be required`,
692
- path: `${paramPath}/required`,
693
- code: "PATH_PARAMETER_NOT_REQUIRED"
694
- });
695
- }
696
- if (!param.schema && !param.content) {
697
- errors.push({
698
- message: `Parameter '${param.name}' missing schema or content`,
699
- path: `${paramPath}`,
700
- code: "MISSING_PARAMETER_SCHEMA"
701
- });
702
- }
703
- }
751
+ static withExample(schema, example) {
752
+ const existingExamples = Array.isArray(schema.examples) ? schema.examples : [];
753
+ return {
754
+ ...schema,
755
+ examples: [...existingExamples, example]
756
+ };
704
757
  }
705
- };
706
-
707
- // src/errors.ts
708
- var OpenAPIToolError = class extends Error {
709
- context;
710
- constructor(message, context) {
711
- super(message);
712
- this.name = this.constructor.name;
713
- this.context = context;
714
- if (Error.captureStackTrace) {
715
- Error.captureStackTrace(this, this.constructor);
716
- }
758
+ /**
759
+ * Add default value to schema
760
+ */
761
+ static withDefault(schema, defaultValue) {
762
+ return {
763
+ ...schema,
764
+ default: defaultValue
765
+ };
717
766
  }
718
- };
719
- var LoadError = class extends OpenAPIToolError {
720
- constructor(message, context) {
721
- super(message, context);
767
+ /**
768
+ * Add format to schema
769
+ */
770
+ static withFormat(schema, format) {
771
+ return {
772
+ ...schema,
773
+ format
774
+ };
722
775
  }
723
- };
724
- var SsrfError = class extends LoadError {
725
- constructor(message, context) {
726
- super(message, context);
776
+ /**
777
+ * Add pattern to schema
778
+ */
779
+ static withPattern(schema, pattern) {
780
+ return {
781
+ ...schema,
782
+ pattern
783
+ };
727
784
  }
728
- };
729
- var ParseError = class extends OpenAPIToolError {
730
- constructor(message, context) {
731
- super(message, context);
785
+ /**
786
+ * Add enum to schema
787
+ */
788
+ static withEnum(schema, values) {
789
+ return {
790
+ ...schema,
791
+ enum: values
792
+ };
732
793
  }
733
- };
734
- var ValidationError = class extends OpenAPIToolError {
735
- errors;
736
- constructor(message, context) {
737
- super(message, context);
738
- this.errors = context?.["errors"];
794
+ /**
795
+ * Add minimum/maximum constraints
796
+ */
797
+ static withRange(schema, min, max, options = {}) {
798
+ const result = { ...schema };
799
+ if (min !== void 0) {
800
+ if (options.exclusive) {
801
+ result.exclusiveMinimum = min;
802
+ } else {
803
+ result.minimum = min;
804
+ }
805
+ }
806
+ if (max !== void 0) {
807
+ if (options.exclusive) {
808
+ result.exclusiveMaximum = max;
809
+ } else {
810
+ result.maximum = max;
811
+ }
812
+ }
813
+ return result;
739
814
  }
740
- };
741
- var GenerationError = class extends OpenAPIToolError {
742
- constructor(message, context) {
743
- super(message, context);
815
+ /**
816
+ * Add minLength/maxLength constraints
817
+ */
818
+ static withLength(schema, minLength, maxLength) {
819
+ const result = { ...schema };
820
+ if (minLength !== void 0) {
821
+ result.minLength = minLength;
822
+ }
823
+ if (maxLength !== void 0) {
824
+ result.maxLength = maxLength;
825
+ }
826
+ return result;
744
827
  }
745
- };
746
- var SchemaError = class extends OpenAPIToolError {
747
- constructor(message, context) {
748
- super(message, context);
828
+ /**
829
+ * Create object schema
830
+ */
831
+ static object(properties, required) {
832
+ return {
833
+ type: "object",
834
+ properties,
835
+ ...required && required.length > 0 && { required },
836
+ additionalProperties: false
837
+ };
749
838
  }
750
- };
751
-
752
- // src/format-resolver.ts
753
- var BUILTIN_FORMAT_RESOLVERS = {
754
- // String formats
755
- uuid: (schema) => ({
756
- ...schema,
757
- 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}$",
758
- description: schema.description || "UUID string (RFC 4122)"
759
- }),
760
- "date-time": (schema) => ({
761
- ...schema,
762
- description: schema.description || "ISO 8601 date-time (e.g., 2024-01-15T09:30:00Z)"
763
- }),
764
- date: (schema) => ({
765
- ...schema,
766
- pattern: schema.pattern ?? "^\\d{4}-\\d{2}-\\d{2}$",
767
- description: schema.description || "ISO 8601 date (e.g., 2024-01-15)"
768
- }),
769
- time: (schema) => ({
770
- ...schema,
771
- pattern: schema.pattern ?? "^\\d{2}:\\d{2}:\\d{2}",
772
- description: schema.description || "ISO 8601 time (e.g., 09:30:00)"
773
- }),
774
- email: (schema) => ({
775
- ...schema,
776
- description: schema.description || "Email address (RFC 5322)"
777
- }),
778
- uri: (schema) => ({
779
- ...schema,
780
- description: schema.description || "URI (RFC 3986)"
781
- }),
782
- "uri-reference": (schema) => ({
783
- ...schema,
784
- description: schema.description || "URI reference (RFC 3986)"
785
- }),
786
- hostname: (schema) => ({
787
- ...schema,
788
- description: schema.description || "Internet hostname (RFC 1123)"
789
- }),
790
- ipv4: (schema) => ({
791
- ...schema,
792
- pattern: schema.pattern ?? "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$",
793
- description: schema.description || "IPv4 address"
794
- }),
795
- ipv6: (schema) => ({
796
- ...schema,
797
- description: schema.description || "IPv6 address (RFC 4291)"
798
- }),
799
- // Integer formats
800
- int32: (schema) => ({
801
- ...schema,
802
- minimum: schema.minimum ?? -2147483648,
803
- maximum: schema.maximum ?? 2147483647
804
- }),
805
- int64: (schema) => ({
806
- ...schema,
807
- minimum: schema.minimum ?? Number.MIN_SAFE_INTEGER,
808
- maximum: schema.maximum ?? Number.MAX_SAFE_INTEGER
809
- }),
810
- // Binary/encoding formats
811
- byte: (schema) => ({
812
- ...schema,
813
- pattern: schema.pattern ?? "^[A-Za-z0-9+/]*={0,2}$",
814
- description: schema.description || "Base64-encoded string (RFC 4648)"
815
- }),
816
- binary: (schema) => ({
817
- ...schema,
818
- description: schema.description || "Binary data"
819
- }),
820
- // Sensitive data formats
821
- password: (schema) => ({
822
- ...schema,
823
- description: schema.description || "Password (sensitive, UI should mask input)"
824
- })
825
- };
826
- function resolveSchemaFormats(schema, resolvers) {
827
- if (!schema || typeof schema !== "object") return schema;
828
- let result = { ...schema };
829
- const format = result["format"];
830
- if (format && resolvers[format]) {
831
- result = { ...resolvers[format](result) };
839
+ /**
840
+ * Create array schema
841
+ */
842
+ static array(items, constraints) {
843
+ return {
844
+ type: "array",
845
+ items,
846
+ ...constraints
847
+ };
832
848
  }
833
- if (result["properties"] && typeof result["properties"] === "object") {
834
- const props = {};
835
- for (const [key, value] of Object.entries(result["properties"])) {
836
- props[key] = resolveSchemaFormats(value, resolvers);
837
- }
838
- result["properties"] = props;
849
+ /**
850
+ * Create string schema
851
+ */
852
+ static string(constraints) {
853
+ return {
854
+ type: "string",
855
+ ...constraints
856
+ };
839
857
  }
840
- if (result["items"]) {
841
- if (Array.isArray(result["items"])) {
842
- result["items"] = result["items"].map((item) => resolveSchemaFormats(item, resolvers));
843
- } else {
844
- result["items"] = resolveSchemaFormats(result["items"], resolvers);
845
- }
858
+ /**
859
+ * Create number schema
860
+ */
861
+ static number(constraints) {
862
+ return {
863
+ type: "number",
864
+ ...constraints
865
+ };
846
866
  }
847
- if (result["additionalProperties"] && typeof result["additionalProperties"] === "object") {
848
- result["additionalProperties"] = resolveSchemaFormats(result["additionalProperties"], resolvers);
867
+ /**
868
+ * Create integer schema
869
+ */
870
+ static integer(constraints) {
871
+ return {
872
+ type: "integer",
873
+ ...constraints
874
+ };
849
875
  }
850
- for (const key of ["allOf", "anyOf", "oneOf"]) {
851
- if (result[key] && Array.isArray(result[key])) {
852
- result[key] = result[key].map((s) => resolveSchemaFormats(s, resolvers));
853
- }
876
+ /**
877
+ * Create boolean schema
878
+ */
879
+ static boolean() {
880
+ return {
881
+ type: "boolean"
882
+ };
854
883
  }
855
- if (result["not"] && typeof result["not"] === "object") {
856
- result["not"] = resolveSchemaFormats(result["not"], resolvers);
884
+ /**
885
+ * Create null schema
886
+ */
887
+ static null() {
888
+ return {
889
+ type: "null"
890
+ };
857
891
  }
858
- return result;
859
- }
860
-
861
- // src/ssrf.ts
862
- var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
863
- "localhost",
864
- "localhost.localdomain",
865
- "ip6-localhost",
866
- "ip6-loopback",
867
- "metadata",
868
- "metadata.google.internal",
869
- "metadata.goog"
870
- ]);
871
- function normalizeSsrfOptions(refResolution) {
872
- return {
873
- allowedHosts: refResolution?.allowedHosts ?? [],
874
- blockedHosts: refResolution?.blockedHosts ?? [],
875
- allowInternalIPs: refResolution?.allowInternalIPs ?? false
876
- };
877
- }
878
- function decodeIpv4MappedIpv6(hostname) {
879
- let h = hostname;
880
- if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
881
- const lower = h.toLowerCase();
882
- const marker = lower.lastIndexOf("::ffff:");
883
- if (marker === -1) return null;
884
- const tail = lower.slice(marker + "::ffff:".length);
885
- if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(tail)) return tail;
886
- const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
887
- if (hex) {
888
- const hi = parseInt(hex[1], 16);
889
- const lo = parseInt(hex[2], 16);
890
- if (Number.isNaN(hi) || Number.isNaN(lo)) return null;
891
- return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
892
+ /**
893
+ * Flatten nested oneOf/anyOf/allOf schemas
894
+ */
895
+ static flatten(schema, maxDepth = 10) {
896
+ if (maxDepth <= 0) return schema;
897
+ const cloned = this.clone(schema);
898
+ if (cloned.oneOf) {
899
+ const flattened = cloned.oneOf.flatMap((s) => {
900
+ const sub = this.flatten(s, maxDepth - 1);
901
+ return sub.oneOf ? sub.oneOf : [sub];
902
+ });
903
+ cloned.oneOf = flattened;
904
+ }
905
+ if (cloned.anyOf) {
906
+ const flattened = cloned.anyOf.flatMap((s) => {
907
+ const sub = this.flatten(s, maxDepth - 1);
908
+ return sub.anyOf ? sub.anyOf : [sub];
909
+ });
910
+ cloned.anyOf = flattened;
911
+ }
912
+ if (cloned.allOf) {
913
+ const flattened = cloned.allOf.flatMap((s) => {
914
+ const sub = this.flatten(s, maxDepth - 1);
915
+ return sub.allOf ? sub.allOf : [sub];
916
+ });
917
+ cloned.allOf = flattened;
918
+ }
919
+ return cloned;
920
+ }
921
+ /**
922
+ * Truncate a schema tree to a maximum nesting depth.
923
+ *
924
+ * The root sits at depth 0; descending into `properties` values, `items`,
925
+ * `additionalProperties`, composition members (`allOf`/`anyOf`/`oneOf`), or
926
+ * `not` increments the depth. Nodes at `maxDepth` keep their scalar keywords
927
+ * (type, description, format, ...) but have their child schemas stripped and
928
+ * a truncation note appended to the description.
929
+ */
930
+ static truncateDepth(schema, maxDepth) {
931
+ const bound = Number.isFinite(maxDepth) ? Math.max(0, Math.floor(maxDepth)) : 10;
932
+ return this.truncateDepthRecursive(schema, 0, bound);
933
+ }
934
+ /** Keys whose value is a map of schemas (JSON Schema 2020-12) */
935
+ static TRUNCATE_MAP_KEYS = [
936
+ "properties",
937
+ "patternProperties",
938
+ "$defs",
939
+ "definitions",
940
+ "dependentSchemas"
941
+ ];
942
+ /** Keys whose value is a single schema (or, for `items`, a tuple array) */
943
+ static TRUNCATE_SCHEMA_KEYS = [
944
+ "items",
945
+ "additionalProperties",
946
+ "not",
947
+ "if",
948
+ "then",
949
+ "else",
950
+ "propertyNames",
951
+ "contains",
952
+ "contentSchema",
953
+ "unevaluatedProperties",
954
+ "unevaluatedItems"
955
+ ];
956
+ /** Keys whose value is an array of schemas */
957
+ static TRUNCATE_LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
958
+ // Copy-on-walk: never mutates the input, only copies nodes that have schema
959
+ // children, and — because the walk is depth-bounded — terminates even on
960
+ // circular schema graphs (which `clone()`'s JSON round-trip would reject).
961
+ static truncateDepthRecursive(node, depth, maxDepth) {
962
+ if (!node || typeof node !== "object") return node;
963
+ const record = node;
964
+ const childKeys = [...this.TRUNCATE_MAP_KEYS, ...this.TRUNCATE_SCHEMA_KEYS, ...this.TRUNCATE_LIST_KEYS];
965
+ const hasChildren = childKeys.some((key) => {
966
+ const value = record[key];
967
+ return value !== null && typeof value === "object";
968
+ });
969
+ if (!hasChildren) return node;
970
+ const copy = { ...node };
971
+ const copyRecord = copy;
972
+ if (depth >= maxDepth) {
973
+ for (const key of childKeys) {
974
+ const value = copyRecord[key];
975
+ if (value !== null && typeof value === "object") {
976
+ delete copyRecord[key];
977
+ }
978
+ }
979
+ delete copyRecord["required"];
980
+ const note = "[Truncated: nested schema exceeds maxSchemaDepth]";
981
+ copy.description = copy.description ? `${copy.description} ${note}` : note;
982
+ return copy;
983
+ }
984
+ for (const key of this.TRUNCATE_MAP_KEYS) {
985
+ const value = copyRecord[key];
986
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
987
+ const mapped = {};
988
+ for (const [name, sub] of Object.entries(value)) {
989
+ mapped[name] = this.truncateDepthRecursive(sub, depth + 1, maxDepth);
990
+ }
991
+ copyRecord[key] = mapped;
992
+ }
993
+ }
994
+ for (const key of this.TRUNCATE_SCHEMA_KEYS) {
995
+ const value = copyRecord[key];
996
+ if (value !== null && typeof value === "object") {
997
+ copyRecord[key] = Array.isArray(value) ? value.map((item) => this.truncateDepthRecursive(item, depth + 1, maxDepth)) : this.truncateDepthRecursive(value, depth + 1, maxDepth);
998
+ }
999
+ }
1000
+ for (const key of this.TRUNCATE_LIST_KEYS) {
1001
+ const value = copyRecord[key];
1002
+ if (Array.isArray(value)) {
1003
+ copyRecord[key] = value.map((member) => this.truncateDepthRecursive(member, depth + 1, maxDepth));
1004
+ }
1005
+ }
1006
+ return copy;
1007
+ }
1008
+ /**
1009
+ * Simplify schema by removing unnecessary fields
1010
+ */
1011
+ static simplify(schema) {
1012
+ const cloned = this.clone(schema);
1013
+ if (Array.isArray(cloned.required) && cloned.required.length === 0) {
1014
+ delete cloned.required;
1015
+ }
1016
+ if (cloned.properties && Object.keys(cloned.properties).length === 0) {
1017
+ delete cloned.properties;
1018
+ }
1019
+ if (Array.isArray(cloned.examples) && cloned.examples.length === 0) {
1020
+ delete cloned.examples;
1021
+ }
1022
+ if (cloned.title && cloned.description && cloned.title === cloned.description) {
1023
+ delete cloned.title;
1024
+ }
1025
+ return cloned;
1026
+ }
1027
+ };
1028
+
1029
+ // src/annotations.ts
1030
+ function inferAnnotationsFromMethod(method) {
1031
+ switch (method) {
1032
+ case "get":
1033
+ case "head":
1034
+ case "options":
1035
+ case "trace":
1036
+ return { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
1037
+ case "put":
1038
+ case "delete":
1039
+ return { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false };
1040
+ case "post":
1041
+ case "patch":
1042
+ return { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
892
1043
  }
893
- return null;
894
1044
  }
895
- function parseIpv4(host) {
896
- const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
897
- if (!m) return null;
898
- const octets = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
899
- if (octets.some((n) => n > 255)) return null;
900
- return octets;
1045
+ var ANNOTATION_KEYS = ["title", "readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"];
1046
+ function pickAnnotations(raw) {
1047
+ if (!raw || typeof raw !== "object") return void 0;
1048
+ const result = {};
1049
+ for (const key of ANNOTATION_KEYS) {
1050
+ const value = raw[key];
1051
+ if (key === "title" ? typeof value === "string" : typeof value === "boolean") {
1052
+ result[key] = value;
1053
+ }
1054
+ }
1055
+ return Object.keys(result).length > 0 ? result : void 0;
901
1056
  }
902
- function isBlockedIpv4(octets) {
903
- const [a, b, c] = octets;
904
- if (a === 0) return true;
905
- if (a === 10) return true;
906
- if (a === 127) return true;
907
- if (a === 100 && b >= 64 && b <= 127) return true;
908
- if (a === 169 && b === 254) return true;
909
- if (a === 172 && b >= 16 && b <= 31) return true;
910
- if (a === 192 && b === 0 && c === 0) return true;
911
- if (a === 192 && b === 168) return true;
912
- if (a === 198 && (b === 18 || b === 19)) return true;
913
- if (a >= 224) return true;
914
- return false;
1057
+ function mergeOverrides(base, layer) {
1058
+ return {
1059
+ ...base,
1060
+ ...layer.disabled !== void 0 && { disabled: layer.disabled },
1061
+ ...layer.name !== void 0 && { name: layer.name },
1062
+ ...layer.title !== void 0 && { title: layer.title },
1063
+ ...layer.description !== void 0 && { description: layer.description },
1064
+ ...(base.annotations || layer.annotations) && {
1065
+ annotations: { ...base.annotations, ...layer.annotations }
1066
+ }
1067
+ };
915
1068
  }
916
- function isBlockedIpv6(host) {
917
- let h = host;
918
- if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
919
- const zone = h.indexOf("%");
920
- if (zone !== -1) h = h.slice(0, zone);
921
- const lower = h.toLowerCase();
922
- if (lower === "::" || lower === "::0") return true;
923
- if (lower === "::1") return true;
924
- if (/^f[cd]/.test(lower)) return true;
925
- if (/^fe[89a-f]/.test(lower)) return true;
926
- if (/^ff/.test(lower)) return true;
927
- return false;
1069
+ function readXMcp(node) {
1070
+ return node["x-mcp"];
928
1071
  }
929
- function isBlockedAddress(host) {
930
- let h = host;
931
- if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
932
- const mapped = decodeIpv4MappedIpv6(host);
933
- if (mapped) {
934
- const o = parseIpv4(mapped);
935
- if (o) return isBlockedIpv4(o);
1072
+ function parseXMcpEnabled(ext) {
1073
+ if (ext === false) return false;
1074
+ if (ext === true) return true;
1075
+ if (ext && typeof ext === "object" && typeof ext.enabled === "boolean") {
1076
+ return ext.enabled;
936
1077
  }
937
- const v4 = parseIpv4(h);
938
- if (v4) return isBlockedIpv4(v4);
939
- if (h.includes(":")) return isBlockedIpv6(h);
940
- return false;
1078
+ return void 0;
941
1079
  }
942
- function isIpLiteral(hostname) {
943
- if (hostname.startsWith("[") && hostname.endsWith("]")) return true;
944
- return parseIpv4(hostname) !== null;
1080
+ function resolveExtensionEnabled(document, pathItem, operation) {
1081
+ let enabled = true;
1082
+ const rootSetting = parseXMcpEnabled(readXMcp(document));
1083
+ if (rootSetting !== void 0) enabled = rootSetting;
1084
+ const pathSetting = parseXMcpEnabled(readXMcp(pathItem));
1085
+ if (pathSetting !== void 0) enabled = pathSetting;
1086
+ const operationDisabled = extractExtensionOverrides(operation).disabled;
1087
+ if (operationDisabled !== void 0) enabled = !operationDisabled;
1088
+ return enabled;
945
1089
  }
946
- function isBlockedHostname(hostname, ssrf) {
947
- if (ssrf.allowInternalIPs) {
948
- return ssrf.blockedHosts.includes(hostname);
1090
+ function extractExtensionOverrides(operation) {
1091
+ const op = operation;
1092
+ let result = {};
1093
+ const speakeasy = op["x-speakeasy-mcp"];
1094
+ if (speakeasy && typeof speakeasy === "object") {
1095
+ const ext = speakeasy;
1096
+ result = mergeOverrides(result, {
1097
+ disabled: typeof ext["disabled"] === "boolean" ? ext["disabled"] : void 0,
1098
+ name: typeof ext["name"] === "string" ? ext["name"] : void 0,
1099
+ title: typeof ext["title"] === "string" ? ext["title"] : void 0,
1100
+ description: typeof ext["description"] === "string" ? ext["description"] : void 0,
1101
+ // Speakeasy's top-level `title` is the tool title, not an annotation slot
1102
+ annotations: pickAnnotations({ ...ext, title: void 0 })
1103
+ });
949
1104
  }
950
- if (ssrf.blockedHosts.includes(hostname)) return true;
951
- const lower = hostname.toLowerCase();
952
- const stripped = lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
953
- if (BLOCKED_HOSTNAMES.has(lower) || BLOCKED_HOSTNAMES.has(stripped)) return true;
954
- return isBlockedAddress(hostname);
955
- }
956
- var defaultLookup = async (hostname) => {
957
- const dns = await import("node:dns");
958
- return dns.promises.lookup(hostname, { all: true });
959
- };
960
- async function assertUrlSafe(url, ssrf, lookup = defaultLookup) {
961
- let parsed;
962
- try {
963
- parsed = new URL(url);
964
- } catch {
965
- throw new SsrfError(`Invalid spec URL: ${url}`, { url });
1105
+ const xMcp = op["x-mcp"];
1106
+ if (xMcp === false) {
1107
+ result = mergeOverrides(result, { disabled: true });
1108
+ } else if (xMcp === true) {
1109
+ result = mergeOverrides(result, { disabled: false });
1110
+ } else if (xMcp && typeof xMcp === "object") {
1111
+ const ext = xMcp;
1112
+ result = mergeOverrides(result, {
1113
+ disabled: typeof ext["enabled"] === "boolean" ? !ext["enabled"] : void 0,
1114
+ name: typeof ext["name"] === "string" ? ext["name"] : void 0,
1115
+ title: typeof ext["title"] === "string" ? ext["title"] : void 0,
1116
+ description: typeof ext["description"] === "string" ? ext["description"] : void 0,
1117
+ annotations: pickAnnotations(ext["annotations"])
1118
+ });
966
1119
  }
967
- const protocol = parsed.protocol.replace(/:$/, "");
968
- if (protocol !== "http" && protocol !== "https") {
969
- throw new SsrfError(`Protocol "${protocol}" is not allowed for network spec loading (only http/https)`, { url });
1120
+ const frontmcp = op["x-frontmcp"];
1121
+ if (frontmcp && typeof frontmcp === "object" && frontmcp.annotations) {
1122
+ const annotations = pickAnnotations(frontmcp.annotations);
1123
+ result = mergeOverrides(result, {
1124
+ annotations,
1125
+ title: typeof frontmcp.annotations.title === "string" ? frontmcp.annotations.title : void 0
1126
+ });
970
1127
  }
971
- const hostname = parsed.hostname;
972
- if (ssrf.allowedHosts.length > 0 && !ssrf.allowedHosts.includes(hostname)) {
973
- throw new SsrfError(`Host "${hostname}" is not in the allowed-hosts list`, { url });
1128
+ return result;
1129
+ }
1130
+
1131
+ // src/client-targets.ts
1132
+ var MAP_KEYS = ["properties", "patternProperties", "dependentSchemas"];
1133
+ var SCHEMA_KEYS = [
1134
+ "items",
1135
+ "additionalProperties",
1136
+ "not",
1137
+ "if",
1138
+ "then",
1139
+ "else",
1140
+ "propertyNames",
1141
+ "contains",
1142
+ "contentSchema",
1143
+ "unevaluatedItems",
1144
+ "unevaluatedProperties"
1145
+ ];
1146
+ var LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
1147
+ function isSchemaObject(value) {
1148
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1149
+ }
1150
+ function walkSchema(node, visit) {
1151
+ if (!isSchemaObject(node)) return node;
1152
+ const visited = visit({ ...node });
1153
+ for (const key of MAP_KEYS) {
1154
+ const value = visited[key];
1155
+ if (isSchemaObject(value)) {
1156
+ const mapped = {};
1157
+ for (const [name, sub] of Object.entries(value)) {
1158
+ mapped[name] = walkSchema(sub, visit);
1159
+ }
1160
+ visited[key] = mapped;
1161
+ }
974
1162
  }
975
- if (ssrf.allowInternalIPs) {
976
- if (ssrf.blockedHosts.includes(hostname)) {
977
- throw new SsrfError(`Host "${hostname}" is blocked`, { url });
1163
+ for (const key of SCHEMA_KEYS) {
1164
+ const value = visited[key];
1165
+ if (Array.isArray(value)) {
1166
+ visited[key] = value.map((item) => walkSchema(item, visit));
1167
+ } else if (isSchemaObject(value)) {
1168
+ visited[key] = walkSchema(value, visit);
978
1169
  }
979
- return;
980
1170
  }
981
- if (isBlockedHostname(hostname, ssrf)) {
982
- throw new SsrfError(`Host "${hostname}" maps to a blocked internal address`, { url });
1171
+ for (const key of LIST_KEYS) {
1172
+ const value = visited[key];
1173
+ if (Array.isArray(value)) {
1174
+ visited[key] = value.map((member) => walkSchema(member, visit));
1175
+ }
983
1176
  }
984
- if (!isIpLiteral(hostname)) {
985
- let addresses;
986
- try {
987
- addresses = await lookup(hostname);
988
- } catch {
989
- return;
1177
+ return visited;
1178
+ }
1179
+ function inlineLocalRefs(schema) {
1180
+ if (!isSchemaObject(schema)) return schema;
1181
+ const root = schema;
1182
+ const resolvePointer = (pointer) => {
1183
+ const parts = pointer.replace(/^#\/?/, "").split("/").filter((part) => part.length > 0).map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"));
1184
+ let current = root;
1185
+ for (const part of parts) {
1186
+ if (!isSchemaObject(current)) return void 0;
1187
+ current = current[part];
1188
+ }
1189
+ return current;
1190
+ };
1191
+ const inline = (node, seenPointers) => {
1192
+ if (!isSchemaObject(node)) return node;
1193
+ const record = node;
1194
+ const ref = record["$ref"];
1195
+ if (typeof ref === "string" && !ref.startsWith("#")) {
1196
+ const { $ref: _external, ...siblings } = record;
1197
+ return inline(
1198
+ { description: `[External $ref ${ref} removed for client compatibility]`, ...siblings },
1199
+ seenPointers
1200
+ );
990
1201
  }
991
- for (const { address } of addresses) {
992
- if (isBlockedAddress(address)) {
993
- throw new SsrfError(`Host "${hostname}" resolves to blocked address ${address}`, { url });
1202
+ if (typeof ref === "string") {
1203
+ const { $ref: _ref, ...siblings } = record;
1204
+ if (seenPointers.has(ref)) {
1205
+ return { description: "[Circular $ref removed for client compatibility]", ...siblings };
1206
+ }
1207
+ const resolved = resolvePointer(ref);
1208
+ if (!isSchemaObject(resolved)) {
1209
+ return { description: `[Unresolvable $ref ${ref} removed for client compatibility]`, ...siblings };
1210
+ }
1211
+ const inlined = inline(resolved, /* @__PURE__ */ new Set([...seenPointers, ref]));
1212
+ if (!isSchemaObject(inlined)) return inlined;
1213
+ return { ...inlined, ...siblings };
1214
+ }
1215
+ const copy = { ...record };
1216
+ delete copy["$defs"];
1217
+ delete copy["definitions"];
1218
+ for (const key of MAP_KEYS) {
1219
+ const value = copy[key];
1220
+ if (isSchemaObject(value)) {
1221
+ const mapped = {};
1222
+ for (const [name, sub] of Object.entries(value)) {
1223
+ mapped[name] = inline(sub, seenPointers);
1224
+ }
1225
+ copy[key] = mapped;
994
1226
  }
995
1227
  }
996
- }
1228
+ for (const key of SCHEMA_KEYS) {
1229
+ const value = copy[key];
1230
+ if (Array.isArray(value)) {
1231
+ copy[key] = value.map((item) => inline(item, seenPointers));
1232
+ } else if (isSchemaObject(value)) {
1233
+ copy[key] = inline(value, seenPointers);
1234
+ }
1235
+ }
1236
+ for (const key of LIST_KEYS) {
1237
+ const value = copy[key];
1238
+ if (Array.isArray(value)) {
1239
+ copy[key] = value.map((member) => inline(member, seenPointers));
1240
+ }
1241
+ }
1242
+ return copy;
1243
+ };
1244
+ return inline(schema, /* @__PURE__ */ new Set());
997
1245
  }
998
- async function safeFetch(url, opts) {
999
- const { headers, timeoutMs = 3e4, followRedirects = true, maxRedirects = 5, ssrf, lookup } = opts;
1000
- const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
1001
- if (typeof fetchImpl !== "function") {
1002
- throw new SsrfError("No fetch implementation available to load OpenAPI spec from URL", { url });
1246
+ function ensureArrayItems(schema) {
1247
+ return walkSchema(schema, (node) => {
1248
+ const type = node["type"];
1249
+ const isArray = type === "array" || Array.isArray(type) && type.includes("array");
1250
+ if (isArray && node["items"] === void 0) {
1251
+ return { ...node, items: {} };
1252
+ }
1253
+ return node;
1254
+ });
1255
+ }
1256
+ function mergeAllOf(node) {
1257
+ const members = node["allOf"];
1258
+ const merged = {};
1259
+ const properties = {};
1260
+ const required = /* @__PURE__ */ new Set();
1261
+ for (const rawMember of members) {
1262
+ if (!isSchemaObject(rawMember)) continue;
1263
+ const member = Array.isArray(rawMember["allOf"]) ? mergeAllOf(rawMember) : rawMember;
1264
+ const { properties: memberProps, required: memberRequired, ...scalars } = member;
1265
+ Object.assign(merged, scalars);
1266
+ if (isSchemaObject(memberProps)) Object.assign(properties, memberProps);
1267
+ if (Array.isArray(memberRequired)) memberRequired.forEach((field) => required.add(String(field)));
1268
+ }
1269
+ const { allOf: _allOf, properties: ownProps, required: ownRequired, ...rest } = node;
1270
+ Object.assign(merged, rest);
1271
+ if (isSchemaObject(ownProps)) Object.assign(properties, ownProps);
1272
+ if (Array.isArray(ownRequired)) ownRequired.forEach((field) => required.add(String(field)));
1273
+ if (Object.keys(properties).length > 0) merged["properties"] = properties;
1274
+ if (required.size > 0) merged["required"] = [...required];
1275
+ return merged;
1276
+ }
1277
+ function nullableWrapperMember(node) {
1278
+ const anyOf = node["anyOf"];
1279
+ if (!Array.isArray(anyOf) || anyOf.length !== 2) return void 0;
1280
+ const nullIndex = anyOf.findIndex((m) => isSchemaObject(m) && m["type"] === "null");
1281
+ if (nullIndex === -1) return void 0;
1282
+ const other = anyOf[1 - nullIndex];
1283
+ return isSchemaObject(other) ? other : void 0;
1284
+ }
1285
+ function describeVariants(members) {
1286
+ return members.map((member, index) => {
1287
+ if (!isSchemaObject(member)) return `variant ${index + 1}`;
1288
+ const record = member;
1289
+ return typeof record["title"] === "string" && record["title"] || typeof record["description"] === "string" && record["description"] || typeof record["type"] === "string" && `type ${record["type"]}` || `variant ${index + 1}`;
1290
+ }).join("; ");
1291
+ }
1292
+ function collapseRootCompositions(schema) {
1293
+ if (!isSchemaObject(schema)) return schema;
1294
+ const node = { ...schema };
1295
+ if (Array.isArray(node["allOf"])) {
1296
+ return collapseRootCompositions(mergeAllOf(node));
1297
+ }
1298
+ const nullableMember = nullableWrapperMember(node);
1299
+ if (nullableMember) {
1300
+ const { anyOf: _anyOf, ...rest } = node;
1301
+ const merged = { ...nullableMember, ...rest };
1302
+ const note = "May be null.";
1303
+ merged["description"] = merged["description"] ? `${merged["description"]} ${note}` : note;
1304
+ return merged;
1003
1305
  }
1004
- let current = url;
1005
- for (let hop = 0; hop <= maxRedirects; hop++) {
1006
- await assertUrlSafe(current, ssrf, lookup);
1007
- const controller = new AbortController();
1008
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1009
- let response;
1010
- try {
1011
- response = await fetchImpl(current, {
1012
- headers,
1013
- signal: controller.signal,
1014
- redirect: "manual"
1015
- });
1016
- } finally {
1017
- clearTimeout(timer);
1306
+ for (const key of ["oneOf", "anyOf"]) {
1307
+ const members = node[key];
1308
+ if (Array.isArray(members)) {
1309
+ const { [key]: _members, ...rest } = node;
1310
+ return {
1311
+ ...rest,
1312
+ description: `${typeof rest["description"] === "string" ? `${rest["description"]} ` : ""}Accepts one of ${members.length} variants: ${describeVariants(members)}.`,
1313
+ "x-variants": members
1314
+ };
1018
1315
  }
1019
- const status = typeof response.status === "number" ? response.status : 0;
1020
- const isRedirect = status >= 300 && status < 400 && status !== 304;
1021
- if (!isRedirect || !followRedirects) {
1022
- return response;
1316
+ }
1317
+ return node;
1318
+ }
1319
+ function collapseNestedUnions(schema) {
1320
+ return walkSchema(schema, (node) => {
1321
+ let current = node;
1322
+ for (; ; ) {
1323
+ if (Array.isArray(current["allOf"])) {
1324
+ current = mergeAllOf(current);
1325
+ continue;
1326
+ }
1327
+ const type = current["type"];
1328
+ if (Array.isArray(type)) {
1329
+ const nonNull = type.filter((t) => t !== "null");
1330
+ const notes = [];
1331
+ if (nonNull.length > 1) notes.push(`Alternative types accepted: ${nonNull.slice(1).join(", ")}.`);
1332
+ if (nonNull.length !== type.length) notes.push("May be null.");
1333
+ current = { ...current, type: nonNull[0] ?? "null" };
1334
+ if (notes.length > 0) {
1335
+ const joined = notes.join(" ");
1336
+ current["description"] = current["description"] ? `${current["description"]} ${joined}` : joined;
1337
+ }
1338
+ continue;
1339
+ }
1340
+ const nullableMember = nullableWrapperMember(current);
1341
+ if (nullableMember) {
1342
+ const { anyOf: _anyOf, ...rest } = current;
1343
+ const merged = { ...nullableMember, ...rest };
1344
+ const note = "May be null.";
1345
+ merged["description"] = merged["description"] ? `${merged["description"]} ${note}` : note;
1346
+ current = merged;
1347
+ continue;
1348
+ }
1349
+ let collapsedUnion = false;
1350
+ for (const key of ["oneOf", "anyOf"]) {
1351
+ const members = current[key];
1352
+ if (Array.isArray(members) && members.length > 0 && isSchemaObject(members[0])) {
1353
+ const { [key]: _members, ...rest } = current;
1354
+ const first = { ...members[0] };
1355
+ const note = members.length > 1 ? `${members.length - 1} alternative schema variant(s) omitted for client compatibility: ${describeVariants(
1356
+ members.slice(1)
1357
+ )}.` : void 0;
1358
+ const merged = { ...first, ...rest };
1359
+ if (note) {
1360
+ merged["description"] = merged["description"] ? `${merged["description"]} ${note}` : note;
1361
+ }
1362
+ current = merged;
1363
+ collapsedUnion = true;
1364
+ break;
1365
+ }
1366
+ }
1367
+ if (collapsedUnion) continue;
1368
+ return current;
1023
1369
  }
1024
- const location = response.headers?.get?.("location") ?? void 0;
1025
- if (!location) {
1026
- return response;
1370
+ });
1371
+ }
1372
+ var GEMINI_SUPPORTED_FORMATS = /* @__PURE__ */ new Set(["date-time", "enum"]);
1373
+ var GEMINI_NUMERIC_FORMATS = /* @__PURE__ */ new Set(["int32", "int64", "float", "double"]);
1374
+ function isNumericNode(node) {
1375
+ const type = node["type"];
1376
+ return type === "integer" || type === "number" || Array.isArray(type) && (type.includes("integer") || type.includes("number"));
1377
+ }
1378
+ function demoteFormats(schema, supported = GEMINI_SUPPORTED_FORMATS) {
1379
+ return walkSchema(schema, (node) => {
1380
+ const format = node["format"];
1381
+ if (typeof format !== "string" || supported.has(format)) return node;
1382
+ if (GEMINI_NUMERIC_FORMATS.has(format) && isNumericNode(node)) return node;
1383
+ const { format: _format, ...rest } = node;
1384
+ const note = `(format: ${format})`;
1385
+ rest["description"] = rest["description"] ? `${rest["description"]} ${note}` : note;
1386
+ return rest;
1387
+ });
1388
+ }
1389
+ function isObjectNode(node) {
1390
+ const type = node["type"];
1391
+ return type === "object" || Array.isArray(type) && type.includes("object") || type === void 0 && isSchemaObject(node["properties"]);
1392
+ }
1393
+ function enforceClosedObjects(schema) {
1394
+ return walkSchema(schema, (node) => {
1395
+ if (isObjectNode(node) && (node["additionalProperties"] === void 0 || node["additionalProperties"] === true)) {
1396
+ return { ...node, additionalProperties: false };
1027
1397
  }
1028
- current = new URL(location, current).toString();
1398
+ return node;
1399
+ });
1400
+ }
1401
+ function requireAllProperties(schema) {
1402
+ return walkSchema(schema, (node) => {
1403
+ if (!isObjectNode(node) || !isSchemaObject(node["properties"])) return node;
1404
+ const properties = node["properties"];
1405
+ const originallyRequired = new Set(Array.isArray(node["required"]) ? node["required"].map(String) : []);
1406
+ const rewritten = {};
1407
+ for (const [name, propSchema] of Object.entries(properties)) {
1408
+ if (originallyRequired.has(name) || !isSchemaObject(propSchema) || propSchema["const"] !== void 0) {
1409
+ rewritten[name] = propSchema;
1410
+ continue;
1411
+ }
1412
+ const prop = propSchema;
1413
+ const withNullEnum = (next) => {
1414
+ const enumValues = next["enum"];
1415
+ if (Array.isArray(enumValues) && !enumValues.includes(null)) {
1416
+ return { ...next, enum: [...enumValues, null] };
1417
+ }
1418
+ return next;
1419
+ };
1420
+ const type = prop["type"];
1421
+ if (typeof type === "string" && type !== "null") {
1422
+ rewritten[name] = withNullEnum({ ...prop, type: [type, "null"] });
1423
+ } else if (Array.isArray(type) && !type.includes("null")) {
1424
+ rewritten[name] = withNullEnum({ ...prop, type: [...type, "null"] });
1425
+ } else {
1426
+ rewritten[name] = withNullEnum(prop);
1427
+ }
1428
+ }
1429
+ return { ...node, properties: rewritten, required: Object.keys(properties) };
1430
+ });
1431
+ }
1432
+ function applyClientTarget(schema, target) {
1433
+ let result = inlineLocalRefs(schema);
1434
+ result = ensureArrayItems(result);
1435
+ if (target === "gemini") {
1436
+ result = collapseNestedUnions(result);
1437
+ result = demoteFormats(result);
1438
+ return result;
1029
1439
  }
1030
- throw new SsrfError(`Too many redirects while loading OpenAPI spec (max ${maxRedirects})`, { url });
1440
+ result = collapseRootCompositions(result);
1441
+ if (target === "openai") {
1442
+ result = enforceClosedObjects(result);
1443
+ result = requireAllProperties(result);
1444
+ }
1445
+ return result;
1031
1446
  }
1032
1447
 
1033
- // src/generator.ts
1034
- var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1035
- document;
1036
- dereferencedDocument;
1037
- options;
1038
- /**
1039
- * Private constructor - use static factory methods to create instances
1040
- */
1041
- constructor(document, options = {}) {
1042
- this.document = document;
1043
- this.options = {
1044
- dereference: options.dereference ?? true,
1045
- baseUrl: options.baseUrl ?? "",
1046
- headers: options.headers ?? {},
1047
- timeout: options.timeout ?? 3e4,
1048
- validate: options.validate ?? true,
1049
- followRedirects: options.followRedirects ?? true,
1050
- refResolution: options.refResolution ?? {}
1051
- };
1052
- }
1448
+ // src/validator.ts
1449
+ var Validator = class {
1053
1450
  /**
1054
- * Create generator from a URL
1451
+ * Validate an OpenAPI document
1055
1452
  */
1056
- static async fromURL(url, options = {}) {
1057
- try {
1058
- const response = await safeFetch(url, {
1059
- headers: options.headers,
1060
- timeoutMs: options.timeout ?? 3e4,
1061
- followRedirects: options.followRedirects ?? true,
1062
- ssrf: normalizeSsrfOptions(options.refResolution)
1453
+ async validate(document) {
1454
+ const errors = [];
1455
+ const warnings = [];
1456
+ if (!document.openapi) {
1457
+ errors.push({
1458
+ message: "Missing required field: openapi",
1459
+ path: "/openapi",
1460
+ code: "MISSING_OPENAPI_VERSION"
1063
1461
  });
1064
- if (!response.ok) {
1065
- throw new LoadError(`Failed to fetch OpenAPI spec from URL: ${response.status} ${response.statusText}`, {
1066
- url,
1067
- status: response.status
1462
+ } else if (!this.isValidOpenAPIVersion(document.openapi)) {
1463
+ errors.push({
1464
+ message: `Unsupported OpenAPI version: ${document.openapi}. Expected 3.0.x or 3.1.x`,
1465
+ path: "/openapi",
1466
+ code: "INVALID_OPENAPI_VERSION"
1467
+ });
1468
+ }
1469
+ if (!document.info) {
1470
+ errors.push({
1471
+ message: "Missing required field: info",
1472
+ path: "/info",
1473
+ code: "MISSING_INFO"
1474
+ });
1475
+ } else {
1476
+ if (!document.info.title) {
1477
+ errors.push({
1478
+ message: "Missing required field: info.title",
1479
+ path: "/info/title",
1480
+ code: "MISSING_TITLE"
1068
1481
  });
1069
1482
  }
1070
- const contentType = response.headers.get("content-type") || "";
1071
- const text = await response.text();
1072
- let document;
1073
- if (contentType.includes("yaml") || contentType.includes("yml") || url.match(/\.ya?ml$/i)) {
1074
- document = yaml.parse(text);
1075
- } else {
1076
- document = JSON.parse(text);
1077
- }
1078
- return new _OpenAPIToolGenerator(document, options);
1079
- } catch (error) {
1080
- if (error instanceof LoadError) {
1081
- throw error;
1483
+ if (!document.info.version) {
1484
+ errors.push({
1485
+ message: "Missing required field: info.version",
1486
+ path: "/info/version",
1487
+ code: "MISSING_VERSION"
1488
+ });
1082
1489
  }
1083
- const errorMessage = error instanceof Error ? error.message : String(error);
1084
- throw new LoadError(`Failed to load OpenAPI spec from URL: ${errorMessage}`, {
1085
- url,
1086
- originalError: error
1490
+ }
1491
+ if (!document.paths || Object.keys(document.paths).length === 0) {
1492
+ warnings.push({
1493
+ message: "No paths defined in OpenAPI document",
1494
+ path: "/paths",
1495
+ code: "NO_PATHS"
1087
1496
  });
1497
+ } else {
1498
+ this.validatePaths(document.paths, errors, warnings);
1088
1499
  }
1089
- }
1090
- /**
1091
- * Create generator from a file path
1092
- */
1093
- static async fromFile(filePath, options = {}) {
1094
- try {
1095
- const [path, fs] = await Promise.all([import("path"), import("fs/promises")]);
1096
- const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
1097
- const content = await fs.readFile(absolutePath, "utf-8");
1098
- const ext = path.extname(filePath).toLowerCase();
1099
- let document;
1100
- if (ext === ".yaml" || ext === ".yml") {
1101
- document = yaml.parse(content);
1102
- } else if (ext === ".json") {
1103
- document = JSON.parse(content);
1104
- } else {
1105
- try {
1106
- document = JSON.parse(content);
1107
- } catch {
1108
- document = yaml.parse(content);
1109
- }
1110
- }
1111
- return new _OpenAPIToolGenerator(document, options);
1112
- } catch (error) {
1113
- const errorMessage = error instanceof Error ? error.message : String(error);
1114
- throw new LoadError(`Failed to load OpenAPI spec from file: ${errorMessage}`, {
1115
- filePath,
1116
- originalError: error
1500
+ if (!document.servers || document.servers.length === 0) {
1501
+ warnings.push({
1502
+ message: "No servers defined. You may need to provide a baseUrl option.",
1503
+ path: "/servers",
1504
+ code: "NO_SERVERS"
1117
1505
  });
1118
1506
  }
1119
- }
1120
- /**
1121
- * Create generator from a YAML string
1122
- */
1123
- static async fromYAML(yamlString, options = {}) {
1124
- try {
1125
- const document = yaml.parse(yamlString);
1126
- return new _OpenAPIToolGenerator(document, options);
1127
- } catch (error) {
1128
- const errorMessage = error instanceof Error ? error.message : String(error);
1129
- throw new ParseError(`Failed to parse YAML: ${errorMessage}`, {
1130
- originalError: error
1507
+ if (document.security && !document.components?.securitySchemes) {
1508
+ warnings.push({
1509
+ message: "Security requirements defined but no security schemes found",
1510
+ path: "/security",
1511
+ code: "NO_SECURITY_SCHEMES"
1131
1512
  });
1132
1513
  }
1514
+ return {
1515
+ valid: errors.length === 0,
1516
+ errors: errors.length > 0 ? errors : void 0,
1517
+ warnings: warnings.length > 0 ? warnings : void 0
1518
+ };
1133
1519
  }
1134
1520
  /**
1135
- * Create generator from a JSON object
1521
+ * Check if OpenAPI version is valid
1136
1522
  */
1137
- static async fromJSON(json, options = {}) {
1138
- const document = JSON.parse(JSON.stringify(json));
1139
- return new _OpenAPIToolGenerator(document, options);
1523
+ isValidOpenAPIVersion(version) {
1524
+ return /^3\.[01]\.\d+$/.test(version);
1140
1525
  }
1141
1526
  /**
1142
- * Get the OpenAPI document
1527
+ * Validate paths
1143
1528
  */
1144
- getDocument() {
1145
- return this.dereferencedDocument ?? this.document;
1529
+ validatePaths(paths, errors, warnings) {
1530
+ for (const [path, pathItem] of Object.entries(paths)) {
1531
+ if (!pathItem) continue;
1532
+ if (!path.startsWith("/")) {
1533
+ errors.push({
1534
+ message: `Path must start with '/': ${path}`,
1535
+ path: `/paths/${path}`,
1536
+ code: "INVALID_PATH_FORMAT"
1537
+ });
1538
+ }
1539
+ const methods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
1540
+ let hasOperations = false;
1541
+ for (const method of methods) {
1542
+ const operation = pathItem[method];
1543
+ if (operation) {
1544
+ hasOperations = true;
1545
+ this.validateOperation(operation, path, method, errors, warnings);
1546
+ }
1547
+ }
1548
+ if (!hasOperations && !pathItem.$ref) {
1549
+ warnings.push({
1550
+ message: `Path has no operations: ${path}`,
1551
+ path: `/paths/${path}`,
1552
+ code: "NO_OPERATIONS"
1553
+ });
1554
+ }
1555
+ }
1146
1556
  }
1147
1557
  /**
1148
- * Validate the OpenAPI document
1558
+ * Validate an operation
1149
1559
  */
1150
- async validate() {
1151
- const validator = new Validator();
1152
- return validator.validate(this.document);
1560
+ validateOperation(operation, path, method, errors, warnings) {
1561
+ const basePath = `/paths/${path}/${method}`;
1562
+ if (!operation.operationId) {
1563
+ warnings.push({
1564
+ message: `Operation missing operationId: ${method.toUpperCase()} ${path}`,
1565
+ path: `${basePath}/operationId`,
1566
+ code: "NO_OPERATION_ID"
1567
+ });
1568
+ }
1569
+ if (!operation.responses || Object.keys(operation.responses).length === 0) {
1570
+ errors.push({
1571
+ message: `Operation missing responses: ${method.toUpperCase()} ${path}`,
1572
+ path: `${basePath}/responses`,
1573
+ code: "NO_RESPONSES"
1574
+ });
1575
+ }
1576
+ if (operation.parameters) {
1577
+ this.validateParameters(operation.parameters, path, method, errors, warnings);
1578
+ }
1579
+ const pathParams = path.match(/\{([^}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
1580
+ const definedPathParams = new Set(
1581
+ operation.parameters?.filter((p) => p.in === "path").map((p) => p.name) ?? []
1582
+ );
1583
+ for (const param of pathParams) {
1584
+ if (!definedPathParams.has(param)) {
1585
+ errors.push({
1586
+ message: `Path parameter '${param}' not defined in parameters: ${method.toUpperCase()} ${path}`,
1587
+ path: `${basePath}/parameters`,
1588
+ code: "MISSING_PATH_PARAMETER"
1589
+ });
1590
+ }
1591
+ }
1153
1592
  }
1154
- // NOTE: internal/private-address blocking + IPv4-mapped-IPv6 decoding now live
1155
- // in `ssrf.ts` (`isBlockedHostname` / `isBlockedAddress` / `decodeIpv4MappedIpv6`),
1156
- // shared by the spec-URL fetch (`fromURL`) and the `$ref` resolver below, and
1157
- // augmented there with DNS resolution (closing the DNS-name-to-internal bypass)
1158
- // and per-hop redirect re-validation (`safeFetch`).
1159
1593
  /**
1160
- * Build $RefParser options based on refResolution configuration.
1161
- * Defaults: allow http/https, block file://, block internal IPs.
1594
+ * Validate parameters
1162
1595
  */
1163
- buildRefParserOptions() {
1164
- const raw = this.options.refResolution;
1165
- const refOpts = {
1166
- allowedProtocols: raw.allowedProtocols ?? ["http", "https"],
1167
- allowedHosts: raw.allowedHosts ?? [],
1168
- blockedHosts: raw.blockedHosts ?? [],
1169
- allowInternalIPs: raw.allowInternalIPs ?? false
1170
- };
1171
- const allowedProtocols = new Set(refOpts.allowedProtocols);
1172
- const hasNetworkProtocol = allowedProtocols.size > 0 && !([...allowedProtocols].length === 1 && allowedProtocols.has("file"));
1173
- if (allowedProtocols.size === 0) {
1174
- return { resolve: { external: false } };
1175
- }
1176
- const resolveConfig = {
1177
- external: true,
1178
- file: allowedProtocols.has("file") ? void 0 : false
1179
- };
1180
- if (hasNetworkProtocol) {
1181
- const hasHostAllowlist = refOpts.allowedHosts.length > 0;
1182
- const hostAllowSet = new Set(refOpts.allowedHosts);
1183
- resolveConfig["http"] = {
1184
- // SECURITY: never auto-follow HTTP redirects when resolving external
1185
- // `$ref`s. `canRead` validates only the INITIAL URL; the resolver's
1186
- // default redirect-following (up to 5 hops) re-fetches the `Location`
1187
- // target WITHOUT re-invoking `canRead`, so an allowlisted host could
1188
- // 302 → `http://169.254.169.254/...` and smuggle a blocked target past
1189
- // the allow/deny lists. `redirects: 0` refuses the first redirect, and
1190
- // our custom `read` (below) additionally refuses redirects itself.
1191
- redirects: 0,
1192
- // Synchronous gate: protocol, host allow-list, and literal/known
1193
- // internal hosts. DNS names that *resolve* to internal addresses pass
1194
- // here (canRead cannot be async) and are caught in `read` via DNS
1195
- // resolution — closing the `127.0.0.1.nip.io` bypass for `$ref`s too.
1196
- canRead: (file) => {
1197
- try {
1198
- const parsed = new URL(file.url);
1199
- const protocol = parsed.protocol.replace(":", "");
1200
- if (!allowedProtocols.has(protocol)) {
1201
- return false;
1202
- }
1203
- if (hasHostAllowlist && !hostAllowSet.has(parsed.hostname)) {
1204
- return false;
1205
- }
1206
- if (isBlockedHostname(parsed.hostname, refOpts)) {
1207
- return false;
1208
- }
1209
- return true;
1210
- } catch {
1211
- return false;
1212
- }
1213
- },
1214
- // SSRF-safe fetch: resolves DNS and rejects names that map to internal
1215
- // addresses, and refuses redirects. NOTE: deliberately does NOT forward
1216
- // `this.options.headers` (the spec-load credentials) to third-party
1217
- // `$ref` hosts — that would leak the spec's auth token cross-origin.
1218
- read: async (file) => {
1219
- const response = await safeFetch(file.url, {
1220
- timeoutMs: this.options.timeout,
1221
- followRedirects: false,
1222
- ssrf: refOpts
1223
- });
1224
- if (!response.ok) {
1225
- throw new LoadError(
1226
- `Failed to resolve external $ref "${file.url}": ${response.status} ${response.statusText}`,
1227
- { url: file.url, status: response.status }
1228
- );
1229
- }
1230
- return response.text();
1231
- }
1232
- };
1596
+ validateParameters(parameters, path, method, errors, warnings) {
1597
+ const basePath = `/paths/${path}/${method}/parameters`;
1598
+ for (let i = 0; i < parameters.length; i++) {
1599
+ const param = parameters[i];
1600
+ const paramPath = `${basePath}/${i}`;
1601
+ if (!param.name) {
1602
+ errors.push({
1603
+ message: "Parameter missing name",
1604
+ path: `${paramPath}/name`,
1605
+ code: "MISSING_PARAMETER_NAME"
1606
+ });
1607
+ }
1608
+ if (!param.in) {
1609
+ errors.push({
1610
+ message: 'Parameter missing "in" field',
1611
+ path: `${paramPath}/in`,
1612
+ code: "MISSING_PARAMETER_IN"
1613
+ });
1614
+ } else if (!["path", "query", "header", "cookie"].includes(param.in)) {
1615
+ errors.push({
1616
+ message: `Invalid parameter location: ${param.in}`,
1617
+ path: `${paramPath}/in`,
1618
+ code: "INVALID_PARAMETER_IN"
1619
+ });
1620
+ }
1621
+ if (param.in === "path" && !param.required) {
1622
+ errors.push({
1623
+ message: `Path parameter '${param.name}' must be required`,
1624
+ path: `${paramPath}/required`,
1625
+ code: "PATH_PARAMETER_NOT_REQUIRED"
1626
+ });
1627
+ }
1628
+ if (!param.schema && !param.content) {
1629
+ errors.push({
1630
+ message: `Parameter '${param.name}' missing schema or content`,
1631
+ path: `${paramPath}`,
1632
+ code: "MISSING_PARAMETER_SCHEMA"
1633
+ });
1634
+ }
1635
+ }
1636
+ }
1637
+ };
1638
+
1639
+ // src/errors.ts
1640
+ var OpenAPIToolError = class extends Error {
1641
+ context;
1642
+ constructor(message, context) {
1643
+ super(message);
1644
+ this.name = this.constructor.name;
1645
+ this.context = context;
1646
+ if (Error.captureStackTrace) {
1647
+ Error.captureStackTrace(this, this.constructor);
1648
+ }
1649
+ }
1650
+ };
1651
+ var LoadError = class extends OpenAPIToolError {
1652
+ constructor(message, context) {
1653
+ super(message, context);
1654
+ }
1655
+ };
1656
+ var SsrfError = class extends LoadError {
1657
+ constructor(message, context) {
1658
+ super(message, context);
1659
+ }
1660
+ };
1661
+ var ParseError = class extends OpenAPIToolError {
1662
+ constructor(message, context) {
1663
+ super(message, context);
1664
+ }
1665
+ };
1666
+ var ValidationError = class extends OpenAPIToolError {
1667
+ errors;
1668
+ constructor(message, context) {
1669
+ super(message, context);
1670
+ this.errors = context?.["errors"];
1671
+ }
1672
+ };
1673
+ var GenerationError = class extends OpenAPIToolError {
1674
+ constructor(message, context) {
1675
+ super(message, context);
1676
+ }
1677
+ };
1678
+ var RequestBuildError = class extends OpenAPIToolError {
1679
+ constructor(message, context) {
1680
+ super(message, context);
1681
+ }
1682
+ };
1683
+ var SchemaError = class extends OpenAPIToolError {
1684
+ constructor(message, context) {
1685
+ super(message, context);
1686
+ }
1687
+ };
1688
+
1689
+ // src/format-resolver.ts
1690
+ var BUILTIN_FORMAT_RESOLVERS = {
1691
+ // String formats
1692
+ uuid: (schema) => ({
1693
+ ...schema,
1694
+ 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}$",
1695
+ description: schema.description || "UUID string (RFC 4122)"
1696
+ }),
1697
+ "date-time": (schema) => ({
1698
+ ...schema,
1699
+ description: schema.description || "ISO 8601 date-time (e.g., 2024-01-15T09:30:00Z)"
1700
+ }),
1701
+ date: (schema) => ({
1702
+ ...schema,
1703
+ pattern: schema.pattern ?? "^\\d{4}-\\d{2}-\\d{2}$",
1704
+ description: schema.description || "ISO 8601 date (e.g., 2024-01-15)"
1705
+ }),
1706
+ time: (schema) => ({
1707
+ ...schema,
1708
+ pattern: schema.pattern ?? "^\\d{2}:\\d{2}:\\d{2}",
1709
+ description: schema.description || "ISO 8601 time (e.g., 09:30:00)"
1710
+ }),
1711
+ email: (schema) => ({
1712
+ ...schema,
1713
+ description: schema.description || "Email address (RFC 5322)"
1714
+ }),
1715
+ uri: (schema) => ({
1716
+ ...schema,
1717
+ description: schema.description || "URI (RFC 3986)"
1718
+ }),
1719
+ "uri-reference": (schema) => ({
1720
+ ...schema,
1721
+ description: schema.description || "URI reference (RFC 3986)"
1722
+ }),
1723
+ hostname: (schema) => ({
1724
+ ...schema,
1725
+ description: schema.description || "Internet hostname (RFC 1123)"
1726
+ }),
1727
+ ipv4: (schema) => ({
1728
+ ...schema,
1729
+ pattern: schema.pattern ?? "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$",
1730
+ description: schema.description || "IPv4 address"
1731
+ }),
1732
+ ipv6: (schema) => ({
1733
+ ...schema,
1734
+ description: schema.description || "IPv6 address (RFC 4291)"
1735
+ }),
1736
+ // Integer formats
1737
+ int32: (schema) => ({
1738
+ ...schema,
1739
+ minimum: schema.minimum ?? -2147483648,
1740
+ maximum: schema.maximum ?? 2147483647
1741
+ }),
1742
+ int64: (schema) => ({
1743
+ ...schema,
1744
+ minimum: schema.minimum ?? Number.MIN_SAFE_INTEGER,
1745
+ maximum: schema.maximum ?? Number.MAX_SAFE_INTEGER
1746
+ }),
1747
+ // Binary/encoding formats
1748
+ byte: (schema) => ({
1749
+ ...schema,
1750
+ pattern: schema.pattern ?? "^[A-Za-z0-9+/]*={0,2}$",
1751
+ description: schema.description || "Base64-encoded string (RFC 4648)"
1752
+ }),
1753
+ binary: (schema) => ({
1754
+ ...schema,
1755
+ description: schema.description || "Binary data"
1756
+ }),
1757
+ // Sensitive data formats
1758
+ password: (schema) => ({
1759
+ ...schema,
1760
+ description: schema.description || "Password (sensitive, UI should mask input)"
1761
+ })
1762
+ };
1763
+ function resolveSchemaFormats(schema, resolvers) {
1764
+ if (!schema || typeof schema !== "object") return schema;
1765
+ let result = { ...schema };
1766
+ const format = result["format"];
1767
+ if (format && resolvers[format]) {
1768
+ result = { ...resolvers[format](result) };
1769
+ }
1770
+ if (result["properties"] && typeof result["properties"] === "object") {
1771
+ const props = {};
1772
+ for (const [key, value] of Object.entries(result["properties"])) {
1773
+ props[key] = resolveSchemaFormats(value, resolvers);
1774
+ }
1775
+ result["properties"] = props;
1776
+ }
1777
+ if (result["items"]) {
1778
+ if (Array.isArray(result["items"])) {
1779
+ result["items"] = result["items"].map((item) => resolveSchemaFormats(item, resolvers));
1233
1780
  } else {
1234
- resolveConfig["http"] = false;
1781
+ result["items"] = resolveSchemaFormats(result["items"], resolvers);
1235
1782
  }
1236
- return { resolve: resolveConfig };
1237
1783
  }
1238
- /**
1239
- * Does the document contain any EXTERNAL `$ref` (a ref that is not a local
1240
- * JSON-pointer beginning with `#`)? Only external refs require the full
1241
- * `$RefParser` (file/http resolvers, which pull Node builtins). A document
1242
- * with only internal refs can be dereferenced with the runtime-agnostic
1243
- * resolver below — so it works on V8 isolates (Cloudflare Workers) too.
1244
- */
1245
- static hasExternalRefs(node, seen = /* @__PURE__ */ new Set()) {
1246
- if (node === null || typeof node !== "object") return false;
1247
- if (seen.has(node)) return false;
1248
- seen.add(node);
1249
- if (Array.isArray(node)) return node.some((n) => _OpenAPIToolGenerator.hasExternalRefs(n, seen));
1250
- const ref = node.$ref;
1251
- if (typeof ref === "string" && !ref.startsWith("#")) return true;
1252
- return Object.values(node).some(
1253
- (v) => _OpenAPIToolGenerator.hasExternalRefs(v, seen)
1254
- );
1784
+ if (result["additionalProperties"] && typeof result["additionalProperties"] === "object") {
1785
+ result["additionalProperties"] = resolveSchemaFormats(result["additionalProperties"], resolvers);
1786
+ }
1787
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
1788
+ if (result[key] && Array.isArray(result[key])) {
1789
+ result[key] = result[key].map((s) => resolveSchemaFormats(s, resolvers));
1790
+ }
1791
+ }
1792
+ if (result["not"] && typeof result["not"] === "object") {
1793
+ result["not"] = resolveSchemaFormats(result["not"], resolvers);
1794
+ }
1795
+ return result;
1796
+ }
1797
+
1798
+ // src/ssrf.ts
1799
+ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
1800
+ "localhost",
1801
+ "localhost.localdomain",
1802
+ "ip6-localhost",
1803
+ "ip6-loopback",
1804
+ "metadata",
1805
+ "metadata.google.internal",
1806
+ "metadata.goog"
1807
+ ]);
1808
+ function normalizeSsrfOptions(refResolution) {
1809
+ return {
1810
+ allowedHosts: refResolution?.allowedHosts ?? [],
1811
+ blockedHosts: refResolution?.blockedHosts ?? [],
1812
+ allowInternalIPs: refResolution?.allowInternalIPs ?? false
1813
+ };
1814
+ }
1815
+ function decodeIpv4MappedIpv6(hostname) {
1816
+ let h = hostname;
1817
+ if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
1818
+ const lower = h.toLowerCase();
1819
+ const marker = lower.lastIndexOf("::ffff:");
1820
+ if (marker === -1) return null;
1821
+ const tail = lower.slice(marker + "::ffff:".length);
1822
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(tail)) return tail;
1823
+ const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
1824
+ if (hex) {
1825
+ const hi = parseInt(hex[1], 16);
1826
+ const lo = parseInt(hex[2], 16);
1827
+ if (Number.isNaN(hi) || Number.isNaN(lo)) return null;
1828
+ return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
1829
+ }
1830
+ return null;
1831
+ }
1832
+ function parseIpv4(host) {
1833
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
1834
+ if (!m) return null;
1835
+ const octets = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
1836
+ if (octets.some((n) => n > 255)) return null;
1837
+ return octets;
1838
+ }
1839
+ function isBlockedIpv4(octets) {
1840
+ const [a, b, c] = octets;
1841
+ if (a === 0) return true;
1842
+ if (a === 10) return true;
1843
+ if (a === 127) return true;
1844
+ if (a === 100 && b >= 64 && b <= 127) return true;
1845
+ if (a === 169 && b === 254) return true;
1846
+ if (a === 172 && b >= 16 && b <= 31) return true;
1847
+ if (a === 192 && b === 0 && c === 0) return true;
1848
+ if (a === 192 && b === 168) return true;
1849
+ if (a === 198 && (b === 18 || b === 19)) return true;
1850
+ if (a >= 224) return true;
1851
+ return false;
1852
+ }
1853
+ function isBlockedIpv6(host) {
1854
+ let h = host;
1855
+ if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
1856
+ const zone = h.indexOf("%");
1857
+ if (zone !== -1) h = h.slice(0, zone);
1858
+ const lower = h.toLowerCase();
1859
+ if (lower === "::" || lower === "::0") return true;
1860
+ if (lower === "::1") return true;
1861
+ if (/^f[cd]/.test(lower)) return true;
1862
+ if (/^fe[89a-f]/.test(lower)) return true;
1863
+ if (/^ff/.test(lower)) return true;
1864
+ return false;
1865
+ }
1866
+ function isBlockedAddress(host) {
1867
+ let h = host;
1868
+ if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
1869
+ const mapped = decodeIpv4MappedIpv6(host);
1870
+ if (mapped) {
1871
+ const o = parseIpv4(mapped);
1872
+ if (o) return isBlockedIpv4(o);
1873
+ }
1874
+ const v4 = parseIpv4(h);
1875
+ if (v4) return isBlockedIpv4(v4);
1876
+ if (h.includes(":")) return isBlockedIpv6(h);
1877
+ return false;
1878
+ }
1879
+ function isIpLiteral(hostname) {
1880
+ if (hostname.startsWith("[") && hostname.endsWith("]")) return true;
1881
+ return parseIpv4(hostname) !== null;
1882
+ }
1883
+ function isBlockedHostname(hostname, ssrf) {
1884
+ if (ssrf.allowInternalIPs) {
1885
+ return ssrf.blockedHosts.includes(hostname);
1886
+ }
1887
+ if (ssrf.blockedHosts.includes(hostname)) return true;
1888
+ const lower = hostname.toLowerCase();
1889
+ const stripped = lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
1890
+ if (BLOCKED_HOSTNAMES.has(lower) || BLOCKED_HOSTNAMES.has(stripped)) return true;
1891
+ return isBlockedAddress(hostname);
1892
+ }
1893
+ var SsrfResolverUnavailableError = class extends Error {
1894
+ };
1895
+ var defaultLookup = async (hostname) => {
1896
+ let dns;
1897
+ try {
1898
+ dns = await import("node:dns");
1899
+ } catch {
1900
+ throw new SsrfResolverUnavailableError("DNS resolution is unavailable on this runtime");
1901
+ }
1902
+ return dns.promises.lookup(hostname, { all: true });
1903
+ };
1904
+ async function assertUrlSafe(url, ssrf, lookup = defaultLookup) {
1905
+ let parsed;
1906
+ try {
1907
+ parsed = new URL(url);
1908
+ } catch {
1909
+ throw new SsrfError(`Invalid spec URL: ${url}`, { url });
1255
1910
  }
1256
- /**
1257
- * Dereference local (`#/...`) `$ref`s without `$RefParser` — pure, dependency-
1258
- * free, runtime-agnostic. A pointer cache makes circular schemas resolve to a
1259
- * shared reference instead of recursing forever (same contract as `$RefParser`).
1260
- */
1261
- static dereferenceInternal(root) {
1262
- const cache = /* @__PURE__ */ new Map();
1263
- const resolvePointer = (ptr) => {
1264
- const parts = ptr.replace(/^#\/?/, "").split("/").filter((p) => p.length > 0).map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
1265
- let cur = root;
1266
- for (const p of parts) cur = cur?.[p];
1267
- return cur;
1268
- };
1269
- const walk = (node) => {
1270
- if (node === null || typeof node !== "object") return node;
1271
- if (Array.isArray(node)) return node.map(walk);
1272
- const ref = node.$ref;
1273
- if (typeof ref === "string" && ref.startsWith("#")) {
1274
- const cached = cache.get(ref);
1275
- if (cached !== void 0) return cached;
1276
- const placeholder = {};
1277
- cache.set(ref, placeholder);
1278
- const resolved = walk(resolvePointer(ref));
1279
- if (resolved && typeof resolved === "object") Object.assign(placeholder, resolved);
1280
- return placeholder;
1281
- }
1282
- const out = {};
1283
- for (const [k, v] of Object.entries(node)) out[k] = walk(v);
1284
- return out;
1285
- };
1286
- return walk(root);
1911
+ const protocol = parsed.protocol.replace(/:$/, "");
1912
+ if (protocol !== "http" && protocol !== "https") {
1913
+ throw new SsrfError(`Protocol "${protocol}" is not allowed for network spec loading (only http/https)`, { url });
1287
1914
  }
1288
- /**
1289
- * Initialize the generator (dereference if needed, then validate)
1290
- */
1291
- async initialize() {
1292
- if (this.options.dereference && !this.dereferencedDocument) {
1293
- const cloned = JSON.parse(JSON.stringify(this.document));
1294
- if (!_OpenAPIToolGenerator.hasExternalRefs(cloned)) {
1295
- this.dereferencedDocument = _OpenAPIToolGenerator.dereferenceInternal(cloned);
1296
- } else {
1297
- try {
1298
- const { default: $RefParser } = await import("@apidevtools/json-schema-ref-parser");
1299
- const refParserOptions = this.buildRefParserOptions();
1300
- this.dereferencedDocument = await $RefParser.dereference(cloned, refParserOptions);
1301
- } catch (error) {
1302
- const errorMessage = error instanceof Error ? error.message : String(error);
1303
- throw new ParseError(`Failed to dereference OpenAPI document: ${errorMessage}`, {
1304
- originalError: error
1305
- });
1306
- }
1307
- }
1308
- }
1309
- if (this.options.validate) {
1310
- const validator = new Validator();
1311
- const documentToValidate = this.dereferencedDocument ?? this.document;
1312
- const result = await validator.validate(documentToValidate);
1313
- if (!result.valid) {
1314
- throw new ParseError("Invalid OpenAPI document", { errors: result.errors });
1315
- }
1316
- }
1915
+ const hostname = parsed.hostname;
1916
+ if (ssrf.allowedHosts.length > 0 && !ssrf.allowedHosts.includes(hostname)) {
1917
+ throw new SsrfError(`Host "${hostname}" is not in the allowed-hosts list`, { url });
1317
1918
  }
1318
- /**
1319
- * Generate all tools from the OpenAPI specification
1320
- */
1321
- async generateTools(options = {}) {
1322
- await this.initialize();
1323
- const document = this.getDocument();
1324
- const tools = [];
1325
- if (!document.paths) {
1326
- return tools;
1327
- }
1328
- for (const [pathStr, pathItem] of Object.entries(document.paths)) {
1329
- if (!pathItem || "$ref" in pathItem) continue;
1330
- const methods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
1331
- for (const method of methods) {
1332
- const operation = pathItem[method];
1333
- if (!operation) continue;
1334
- if (!this.shouldIncludeOperation(operation, pathStr, method, options)) {
1335
- continue;
1336
- }
1337
- try {
1338
- const tool = await this.generateTool(pathStr, method, options);
1339
- tools.push(tool);
1340
- } catch (error) {
1341
- const errorMessage = error instanceof Error ? error.message : String(error);
1342
- console.warn(`Failed to generate tool for ${method.toUpperCase()} ${pathStr}:`, errorMessage);
1343
- }
1344
- }
1919
+ if (ssrf.allowInternalIPs) {
1920
+ if (ssrf.blockedHosts.includes(hostname)) {
1921
+ throw new SsrfError(`Host "${hostname}" is blocked`, { url });
1345
1922
  }
1346
- return tools;
1923
+ return [];
1347
1924
  }
1348
- /**
1349
- * Generate a specific tool for a path and method
1350
- */
1351
- async generateTool(pathStr, method, options = {}) {
1352
- await this.initialize();
1353
- const document = this.getDocument();
1354
- if (!document.paths) {
1355
- throw new Error("No paths defined in OpenAPI document");
1356
- }
1357
- const pathItem = document.paths[pathStr];
1358
- const operation = pathItem?.[method.toLowerCase()];
1359
- if (!operation) {
1360
- throw new Error(`Operation not found: ${method.toUpperCase()} ${pathStr}`);
1361
- }
1362
- const parameterResolver = new ParameterResolver(options.namingStrategy);
1363
- let pathParameters = void 0;
1364
- if (pathItem.parameters) {
1365
- pathParameters = pathItem.parameters.filter(
1366
- (p) => !isReferenceObject(p)
1367
- );
1368
- }
1369
- let securityRequirements = void 0;
1370
- const securitySpec = operation.security ?? document.security;
1371
- if (securitySpec) {
1372
- securityRequirements = this.extractSecurityRequirements(securitySpec, document);
1373
- }
1374
- const { inputSchema, mapper } = parameterResolver.resolve(
1375
- operation,
1376
- pathParameters,
1377
- securityRequirements,
1378
- options.includeSecurityInInput
1379
- );
1380
- const responseBuilder = new ResponseBuilder(options);
1381
- const outputSchema = responseBuilder.build(operation.responses);
1382
- const name = this.generateToolName(pathStr, method, operation.operationId, options);
1383
- const description = operation.summary || operation.description || `${method.toUpperCase()} ${pathStr}`;
1384
- const metadata = this.extractMetadata(pathStr, method, operation, document, outputSchema);
1385
- const formatResolvers = {
1386
- ...options.resolveFormats ? BUILTIN_FORMAT_RESOLVERS : {},
1387
- ...options.formatResolvers
1388
- };
1389
- const hasFormatResolvers = Object.keys(formatResolvers).length > 0;
1390
- const resolvedInputSchema = hasFormatResolvers ? resolveSchemaFormats(inputSchema, formatResolvers) : inputSchema;
1391
- const resolvedOutputSchema = hasFormatResolvers && outputSchema ? resolveSchemaFormats(outputSchema, formatResolvers) : outputSchema;
1392
- return {
1393
- name,
1394
- description,
1395
- inputSchema: resolvedInputSchema,
1396
- outputSchema: resolvedOutputSchema,
1397
- mapper,
1398
- metadata
1399
- };
1925
+ if (isBlockedHostname(hostname, ssrf)) {
1926
+ throw new SsrfError(`Host "${hostname}" maps to a blocked internal address`, { url });
1400
1927
  }
1401
- /**
1402
- * Check if an operation should be included
1403
- */
1404
- shouldIncludeOperation(operation, path, method, options) {
1405
- if (operation.deprecated && !options.includeDeprecated) {
1406
- return false;
1407
- }
1408
- if (options.includeOperations && operation.operationId) {
1409
- if (!options.includeOperations.includes(operation.operationId)) {
1410
- return false;
1411
- }
1412
- }
1413
- if (options.excludeOperations && operation.operationId) {
1414
- if (options.excludeOperations.includes(operation.operationId)) {
1415
- return false;
1416
- }
1417
- }
1418
- if (options.filterFn) {
1419
- return options.filterFn({
1420
- ...operation,
1421
- path,
1422
- method
1423
- });
1424
- }
1425
- return true;
1928
+ if (isIpLiteral(hostname)) {
1929
+ return [];
1426
1930
  }
1427
- /**
1428
- * Generate a tool name
1429
- */
1430
- generateToolName(path, method, operationId, options = {}) {
1431
- if (options.namingStrategy?.toolNameGenerator) {
1432
- return options.namingStrategy.toolNameGenerator(path, method, operationId);
1433
- }
1434
- if (operationId) {
1435
- return operationId;
1931
+ let addresses;
1932
+ try {
1933
+ addresses = await lookup(hostname);
1934
+ } catch (error) {
1935
+ if (error instanceof SsrfResolverUnavailableError) {
1936
+ return [];
1436
1937
  }
1437
- const sanitized = path.replace(/\{([^}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1438
- return `${method}_${sanitized}`;
1938
+ const message = error instanceof Error ? error.message : String(error);
1939
+ throw new SsrfError(`Host "${hostname}" could not be resolved for SSRF validation: ${message}`, { url });
1439
1940
  }
1440
- /**
1441
- * Extract metadata from operation
1442
- */
1443
- extractMetadata(path, method, operation, document, outputSchema) {
1444
- const metadata = {
1445
- path,
1446
- method,
1447
- operationId: operation.operationId,
1448
- operationSummary: operation.summary,
1449
- operationDescription: operation.description,
1450
- tags: operation.tags,
1451
- deprecated: operation.deprecated
1452
- };
1453
- if (operation.security || document.security) {
1454
- metadata.security = this.extractSecurityRequirements(
1455
- operation.security ?? document.security,
1456
- document
1457
- );
1458
- }
1459
- const servers = operation.servers ?? document.servers;
1460
- if (servers) {
1461
- metadata.servers = servers.map((server) => ({
1462
- url: this.options.baseUrl || server.url,
1463
- description: server.description,
1464
- variables: server.variables
1465
- }));
1466
- } else if (this.options.baseUrl) {
1467
- metadata.servers = [{ url: this.options.baseUrl }];
1468
- }
1469
- const schemaObj = outputSchema;
1470
- if (schemaObj && Array.isArray(schemaObj["oneOf"])) {
1471
- const codes = schemaObj["oneOf"].map((schema) => schema["x-status-code"]).filter((code) => code !== void 0 && code !== null);
1472
- if (codes.length > 0) {
1473
- metadata.responseStatusCodes = codes;
1474
- }
1475
- } else if (schemaObj && schemaObj["x-status-code"] !== void 0 && schemaObj["x-status-code"] !== null) {
1476
- metadata.responseStatusCodes = [schemaObj["x-status-code"]];
1477
- }
1478
- if (operation.externalDocs) {
1479
- metadata.externalDocs = operation.externalDocs;
1480
- }
1481
- const operationWithExt = operation;
1482
- if (operationWithExt["x-frontmcp"]) {
1483
- metadata.frontmcp = operationWithExt["x-frontmcp"];
1941
+ if (addresses.length === 0) {
1942
+ throw new SsrfError(`Host "${hostname}" did not resolve to any address`, { url });
1943
+ }
1944
+ for (const { address } of addresses) {
1945
+ if (isBlockedAddress(address)) {
1946
+ throw new SsrfError(`Host "${hostname}" resolves to blocked address ${address}`, { url });
1484
1947
  }
1485
- return metadata;
1486
1948
  }
1487
- /**
1488
- * Extract security requirements
1489
- */
1490
- extractSecurityRequirements(security, document) {
1491
- if (!security || !document.components?.securitySchemes) {
1492
- return [];
1949
+ return addresses;
1950
+ }
1951
+ var DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
1952
+ async function loadNodeHttpModules() {
1953
+ try {
1954
+ const [http, https] = await Promise.all([import("node:http"), import("node:https")]);
1955
+ return { http, https };
1956
+ } catch {
1957
+ return null;
1958
+ }
1959
+ }
1960
+ function pickHttpModule(protocol, modules) {
1961
+ return protocol === "https:" ? modules.https : modules.http;
1962
+ }
1963
+ function makePinnedLookup(pinned) {
1964
+ return (_hostname, options, callback) => {
1965
+ const done = typeof options === "function" ? options : callback;
1966
+ const wantsAll = typeof options === "object" && options !== null && options.all === true;
1967
+ if (wantsAll) {
1968
+ done(
1969
+ null,
1970
+ pinned.map(({ address, family }) => ({ address, family }))
1971
+ );
1972
+ } else {
1973
+ done(null, pinned[0].address, pinned[0].family);
1493
1974
  }
1494
- return security.flatMap(
1495
- (req) => Object.entries(req).map(([scheme, scopes]) => {
1496
- const securityScheme = document.components.securitySchemes[scheme];
1497
- if (isReferenceObject(securityScheme)) {
1498
- return { scheme, type: "http", scopes };
1975
+ };
1976
+ }
1977
+ var NULL_BODY_STATUS = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
1978
+ function nodePinnedTransport(modules) {
1979
+ return (url, { headers, signal, pinned, maxBytes }) => new Promise((resolve, reject) => {
1980
+ const limit = maxBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
1981
+ const lib = pickHttpModule(new URL(url).protocol, modules);
1982
+ const requestOptions = {
1983
+ method: "GET",
1984
+ signal,
1985
+ headers: { ...headers, "accept-encoding": "identity" }
1986
+ };
1987
+ if (pinned.length > 0) {
1988
+ requestOptions["lookup"] = makePinnedLookup(pinned);
1989
+ }
1990
+ const request = lib.request(url, requestOptions, (response) => {
1991
+ const chunks = [];
1992
+ let received = 0;
1993
+ response.on("data", (chunk) => {
1994
+ received += chunk.length;
1995
+ if (received > limit) {
1996
+ request.destroy();
1997
+ reject(new SsrfError(`Response body exceeds ${limit} bytes`, { url }));
1998
+ return;
1499
1999
  }
1500
- const apiKeyIn = "in" in securityScheme ? securityScheme.in : void 0;
1501
- const result = {
1502
- scheme,
1503
- type: securityScheme.type,
1504
- scopes,
1505
- name: "name" in securityScheme ? securityScheme.name : void 0,
1506
- in: apiKeyIn && (apiKeyIn === "query" || apiKeyIn === "header" || apiKeyIn === "cookie") ? apiKeyIn : void 0
1507
- };
1508
- if (securityScheme.type === "http") {
1509
- result.httpScheme = "scheme" in securityScheme ? securityScheme.scheme : void 0;
1510
- result.bearerFormat = "bearerFormat" in securityScheme ? securityScheme.bearerFormat : void 0;
2000
+ chunks.push(chunk);
2001
+ });
2002
+ response.on("end", () => {
2003
+ const status = response.statusCode;
2004
+ const responseHeaders = new Headers();
2005
+ const entries = Object.entries(response.headers);
2006
+ for (const [key, value] of entries) {
2007
+ if (Array.isArray(value)) {
2008
+ for (const item of value) responseHeaders.append(key, item);
2009
+ } else {
2010
+ responseHeaders.append(key, value);
2011
+ }
1511
2012
  }
1512
- result.description = "description" in securityScheme ? securityScheme.description : void 0;
1513
- return result;
1514
- })
1515
- );
2013
+ const body = NULL_BODY_STATUS.has(status) ? null : Buffer.concat(chunks);
2014
+ resolve(new Response(body, { status, statusText: response.statusMessage, headers: responseHeaders }));
2015
+ });
2016
+ response.on("error", reject);
2017
+ });
2018
+ request.on("error", reject);
2019
+ request.end();
2020
+ });
2021
+ }
2022
+ function fetchTransport(fetchImpl) {
2023
+ return (url, { headers, signal }) => fetchImpl(url, { headers, signal, redirect: "manual" });
2024
+ }
2025
+ async function selectTransport(opts, url) {
2026
+ if (opts.fetchImpl) {
2027
+ return fetchTransport(opts.fetchImpl);
1516
2028
  }
1517
- };
1518
-
1519
- // src/schema-builder.ts
1520
- var SchemaBuilder = class {
1521
- /**
1522
- * Merge multiple schemas into one
1523
- */
1524
- static merge(schemas) {
1525
- if (schemas.length === 0) {
1526
- return { type: "object" };
2029
+ const modules = await loadNodeHttpModules();
2030
+ if (!modules) {
2031
+ const platformFetch = globalThis.fetch;
2032
+ if (typeof platformFetch === "function") {
2033
+ return fetchTransport(platformFetch);
1527
2034
  }
1528
- if (schemas.length === 1) {
1529
- return schemas[0];
2035
+ throw new SsrfError("No fetch implementation available to load OpenAPI spec from URL", { url });
2036
+ }
2037
+ return nodePinnedTransport(modules);
2038
+ }
2039
+ async function safeFetch(url, opts) {
2040
+ const { headers, timeoutMs = 3e4, followRedirects = true, maxRedirects = 5, ssrf, lookup } = opts;
2041
+ const transport = await selectTransport(opts, url);
2042
+ let current = url;
2043
+ for (let hop = 0; hop <= maxRedirects; hop++) {
2044
+ const pinned = await assertUrlSafe(current, ssrf, lookup);
2045
+ const controller = new AbortController();
2046
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2047
+ let response;
2048
+ try {
2049
+ response = await transport(current, { headers, signal: controller.signal, pinned, maxBytes: opts.maxResponseBytes });
2050
+ } finally {
2051
+ clearTimeout(timer);
1530
2052
  }
1531
- const merged = {
1532
- type: "object",
1533
- properties: {},
1534
- required: []
1535
- };
1536
- const allRequired = /* @__PURE__ */ new Set();
1537
- for (const schema of schemas) {
1538
- if (schema.properties) {
1539
- merged.properties = {
1540
- ...merged.properties,
1541
- ...schema.properties
1542
- };
1543
- }
1544
- if (schema.required) {
1545
- schema.required.forEach((field) => allRequired.add(field));
2053
+ const status = typeof response.status === "number" ? response.status : 0;
2054
+ const isRedirect = status >= 300 && status < 400 && status !== 304;
2055
+ if (!isRedirect || !followRedirects) {
2056
+ return response;
2057
+ }
2058
+ const location = response.headers?.get?.("location") ?? void 0;
2059
+ if (!location) {
2060
+ return response;
2061
+ }
2062
+ current = new URL(location, current).toString();
2063
+ }
2064
+ throw new SsrfError(`Too many redirects while loading OpenAPI spec (max ${maxRedirects})`, { url });
2065
+ }
2066
+
2067
+ // src/generator.ts
2068
+ var MCP_MAX_TOOL_NAME_LENGTH = 128;
2069
+ var DEFAULT_MAX_TOOL_NAME_LENGTH = 64;
2070
+ var MAX_NAME_DEDUP_ATTEMPTS = 256;
2071
+ function applySecureDefaults(options) {
2072
+ if (!options.secureDefaults) return options;
2073
+ return {
2074
+ ...options,
2075
+ followRedirects: options.followRedirects ?? false,
2076
+ // Merge PER KEY: a user tightening one refResolution knob (e.g.
2077
+ // blockedHosts) must not silently discard the preset's external-$ref
2078
+ // lockdown. A DEFINED allowedProtocols still wins — but an explicitly
2079
+ // undefined one (programmatic option building) must not defeat the
2080
+ // preset via object spread copying undefined-valued keys.
2081
+ refResolution: {
2082
+ ...options.refResolution,
2083
+ allowedProtocols: options.refResolution?.allowedProtocols ?? []
2084
+ }
2085
+ };
2086
+ }
2087
+ function globToRegExp(glob) {
2088
+ let pattern = "^";
2089
+ for (let i = 0; i < glob.length; i++) {
2090
+ const char = glob[i];
2091
+ if (char === "*") {
2092
+ if (glob[i + 1] === "*") {
2093
+ pattern += ".*";
2094
+ i++;
2095
+ } else {
2096
+ pattern += "[^/]*";
1546
2097
  }
2098
+ } else if (char === "?") {
2099
+ pattern += "[^/]";
2100
+ } else {
2101
+ pattern += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1547
2102
  }
1548
- if (allRequired.size > 0) {
1549
- merged.required = Array.from(allRequired);
2103
+ }
2104
+ return new RegExp(`${pattern}$`);
2105
+ }
2106
+ function matchesAnyGlob(path, globs) {
2107
+ return globs.some((glob) => globToRegExp(glob).test(path));
2108
+ }
2109
+ function fnv1aHex(input) {
2110
+ let hash = 2166136261;
2111
+ for (let i = 0; i < input.length; i++) {
2112
+ hash ^= input.charCodeAt(i);
2113
+ hash = Math.imul(hash, 16777619);
2114
+ }
2115
+ return (hash >>> 0).toString(16).padStart(8, "0");
2116
+ }
2117
+ function normalizeToolName(raw, maxLength, fallbackSeed) {
2118
+ let hashSeed = raw;
2119
+ let name = raw.replace(/[^A-Za-z0-9_.-]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
2120
+ if (name.length === 0) {
2121
+ hashSeed = fallbackSeed;
2122
+ name = `tool_${fnv1aHex(fallbackSeed)}`;
2123
+ }
2124
+ const cap = Math.min(Math.max(1, maxLength), MCP_MAX_TOOL_NAME_LENGTH);
2125
+ if (name.length > cap) {
2126
+ if (cap >= 13) {
2127
+ name = `${name.slice(0, cap - 9)}_${fnv1aHex(hashSeed)}`;
2128
+ } else {
2129
+ name = fnv1aHex(hashSeed).slice(0, cap);
1550
2130
  }
1551
- return merged;
1552
2131
  }
2132
+ return name;
2133
+ }
2134
+ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2135
+ document;
2136
+ dereferencedDocument;
2137
+ options;
1553
2138
  /**
1554
- * Create a union schema (oneOf)
2139
+ * Private constructor - use static factory methods to create instances
1555
2140
  */
1556
- static union(schemas) {
1557
- if (schemas.length === 0) {
1558
- return {};
1559
- }
1560
- if (schemas.length === 1) {
1561
- return schemas[0];
1562
- }
1563
- return {
1564
- oneOf: schemas
2141
+ constructor(document, rawOptions = {}) {
2142
+ this.document = document;
2143
+ const options = applySecureDefaults(rawOptions);
2144
+ this.options = {
2145
+ dereference: options.dereference ?? true,
2146
+ baseUrl: options.baseUrl ?? "",
2147
+ headers: options.headers ?? {},
2148
+ timeout: options.timeout ?? 3e4,
2149
+ validate: options.validate ?? true,
2150
+ followRedirects: options.followRedirects ?? true,
2151
+ refResolution: options.refResolution ?? {},
2152
+ secureDefaults: options.secureDefaults ?? false
1565
2153
  };
1566
2154
  }
1567
2155
  /**
1568
- * Deep clone a schema
2156
+ * Create generator from a URL
1569
2157
  */
1570
- static clone(schema) {
1571
- return JSON.parse(JSON.stringify(schema));
2158
+ static async fromURL(url, rawOptions = {}) {
2159
+ const options = applySecureDefaults(rawOptions);
2160
+ try {
2161
+ const response = await safeFetch(url, {
2162
+ headers: options.headers,
2163
+ timeoutMs: options.timeout ?? 3e4,
2164
+ followRedirects: options.followRedirects ?? true,
2165
+ ssrf: normalizeSsrfOptions(options.refResolution)
2166
+ });
2167
+ if (!response.ok) {
2168
+ throw new LoadError(`Failed to fetch OpenAPI spec from URL: ${response.status} ${response.statusText}`, {
2169
+ url,
2170
+ status: response.status
2171
+ });
2172
+ }
2173
+ const contentType = response.headers.get("content-type") || "";
2174
+ const text = await response.text();
2175
+ let document;
2176
+ if (contentType.includes("yaml") || contentType.includes("yml") || url.match(/\.ya?ml$/i)) {
2177
+ document = yaml.parse(text);
2178
+ } else {
2179
+ document = JSON.parse(text);
2180
+ }
2181
+ return new _OpenAPIToolGenerator(document, options);
2182
+ } catch (error) {
2183
+ if (error instanceof LoadError) {
2184
+ throw error;
2185
+ }
2186
+ const errorMessage = error instanceof Error ? error.message : String(error);
2187
+ throw new LoadError(`Failed to load OpenAPI spec from URL: ${errorMessage}`, {
2188
+ url,
2189
+ originalError: error
2190
+ });
2191
+ }
1572
2192
  }
1573
2193
  /**
1574
- * Remove $ref from schema (assumes already dereferenced)
2194
+ * Create generator from a file path
1575
2195
  */
1576
- static removeRefs(schema) {
1577
- const cloned = this.clone(schema);
1578
- this.removeRefsRecursive(cloned);
1579
- return cloned;
1580
- }
1581
- static removeRefsRecursive(obj) {
1582
- if (!obj || typeof obj !== "object") return;
1583
- if (obj.$ref) {
1584
- delete obj.$ref;
1585
- }
1586
- for (const key in obj) {
1587
- if (key in obj) {
1588
- const value = obj[key];
1589
- if (value && typeof value === "object") {
1590
- this.removeRefsRecursive(value);
2196
+ static async fromFile(filePath, options = {}) {
2197
+ try {
2198
+ const [path, fs] = await Promise.all([import("path"), import("fs/promises")]);
2199
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
2200
+ const content = await fs.readFile(absolutePath, "utf-8");
2201
+ const ext = path.extname(filePath).toLowerCase();
2202
+ let document;
2203
+ if (ext === ".yaml" || ext === ".yml") {
2204
+ document = yaml.parse(content);
2205
+ } else if (ext === ".json") {
2206
+ document = JSON.parse(content);
2207
+ } else {
2208
+ try {
2209
+ document = JSON.parse(content);
2210
+ } catch {
2211
+ document = yaml.parse(content);
1591
2212
  }
1592
2213
  }
2214
+ return new _OpenAPIToolGenerator(document, options);
2215
+ } catch (error) {
2216
+ const errorMessage = error instanceof Error ? error.message : String(error);
2217
+ throw new LoadError(`Failed to load OpenAPI spec from file: ${errorMessage}`, {
2218
+ filePath,
2219
+ originalError: error
2220
+ });
1593
2221
  }
1594
2222
  }
1595
2223
  /**
1596
- * Add description to schema
2224
+ * Create generator from a YAML string
1597
2225
  */
1598
- static withDescription(schema, description) {
1599
- return {
1600
- ...schema,
1601
- description
1602
- };
2226
+ static async fromYAML(yamlString, options = {}) {
2227
+ try {
2228
+ const document = yaml.parse(yamlString);
2229
+ return new _OpenAPIToolGenerator(document, options);
2230
+ } catch (error) {
2231
+ const errorMessage = error instanceof Error ? error.message : String(error);
2232
+ throw new ParseError(`Failed to parse YAML: ${errorMessage}`, {
2233
+ originalError: error
2234
+ });
2235
+ }
1603
2236
  }
1604
2237
  /**
1605
- * Add example to schema
2238
+ * Create generator from a JSON object
1606
2239
  */
1607
- static withExample(schema, example) {
1608
- const existingExamples = Array.isArray(schema.examples) ? schema.examples : [];
1609
- return {
1610
- ...schema,
1611
- examples: [...existingExamples, example]
1612
- };
2240
+ static async fromJSON(json, options = {}) {
2241
+ const document = JSON.parse(JSON.stringify(json));
2242
+ return new _OpenAPIToolGenerator(document, options);
1613
2243
  }
1614
2244
  /**
1615
- * Add default value to schema
2245
+ * Get the OpenAPI document
1616
2246
  */
1617
- static withDefault(schema, defaultValue) {
1618
- return {
1619
- ...schema,
1620
- default: defaultValue
1621
- };
2247
+ getDocument() {
2248
+ return this.dereferencedDocument ?? this.document;
1622
2249
  }
1623
2250
  /**
1624
- * Add format to schema
2251
+ * Validate the OpenAPI document
2252
+ */
2253
+ async validate() {
2254
+ const validator = new Validator();
2255
+ return validator.validate(this.document);
2256
+ }
2257
+ // NOTE: internal/private-address blocking + IPv4-mapped-IPv6 decoding now live
2258
+ // in `ssrf.ts` (`isBlockedHostname` / `isBlockedAddress` / `decodeIpv4MappedIpv6`),
2259
+ // shared by the spec-URL fetch (`fromURL`) and the `$ref` resolver below, and
2260
+ // augmented there with DNS resolution (closing the DNS-name-to-internal bypass)
2261
+ // and per-hop redirect re-validation (`safeFetch`).
2262
+ /**
2263
+ * Build $RefParser options based on refResolution configuration.
2264
+ * Defaults: allow http/https, block file://, block internal IPs.
1625
2265
  */
1626
- static withFormat(schema, format) {
1627
- return {
1628
- ...schema,
1629
- format
2266
+ buildRefParserOptions() {
2267
+ const raw = this.options.refResolution;
2268
+ const refOpts = {
2269
+ allowedProtocols: raw.allowedProtocols ?? ["http", "https"],
2270
+ allowedHosts: raw.allowedHosts ?? [],
2271
+ blockedHosts: raw.blockedHosts ?? [],
2272
+ allowInternalIPs: raw.allowInternalIPs ?? false
2273
+ };
2274
+ const allowedProtocols = new Set(refOpts.allowedProtocols);
2275
+ const hasNetworkProtocol = allowedProtocols.size > 0 && !([...allowedProtocols].length === 1 && allowedProtocols.has("file"));
2276
+ if (allowedProtocols.size === 0) {
2277
+ return { resolve: { external: false } };
2278
+ }
2279
+ const resolveConfig = {
2280
+ external: true,
2281
+ file: allowedProtocols.has("file") ? void 0 : false
1630
2282
  };
2283
+ if (hasNetworkProtocol) {
2284
+ const hasHostAllowlist = refOpts.allowedHosts.length > 0;
2285
+ const hostAllowSet = new Set(refOpts.allowedHosts);
2286
+ resolveConfig["http"] = {
2287
+ // SECURITY: never auto-follow HTTP redirects when resolving external
2288
+ // `$ref`s. `canRead` validates only the INITIAL URL; the resolver's
2289
+ // default redirect-following (up to 5 hops) re-fetches the `Location`
2290
+ // target WITHOUT re-invoking `canRead`, so an allowlisted host could
2291
+ // 302 → `http://169.254.169.254/...` and smuggle a blocked target past
2292
+ // the allow/deny lists. `redirects: 0` refuses the first redirect, and
2293
+ // our custom `read` (below) additionally refuses redirects itself.
2294
+ redirects: 0,
2295
+ // Synchronous gate: protocol, host allow-list, and literal/known
2296
+ // internal hosts. DNS names that *resolve* to internal addresses pass
2297
+ // here (canRead cannot be async) and are caught in `read` via DNS
2298
+ // resolution — closing the `127.0.0.1.nip.io` bypass for `$ref`s too.
2299
+ canRead: (file) => {
2300
+ try {
2301
+ const parsed = new URL(file.url);
2302
+ const protocol = parsed.protocol.replace(":", "");
2303
+ if (!allowedProtocols.has(protocol)) {
2304
+ return false;
2305
+ }
2306
+ if (hasHostAllowlist && !hostAllowSet.has(parsed.hostname)) {
2307
+ return false;
2308
+ }
2309
+ if (isBlockedHostname(parsed.hostname, refOpts)) {
2310
+ return false;
2311
+ }
2312
+ return true;
2313
+ } catch {
2314
+ return false;
2315
+ }
2316
+ },
2317
+ // SSRF-safe fetch: resolves DNS and rejects names that map to internal
2318
+ // addresses, and refuses redirects. NOTE: deliberately does NOT forward
2319
+ // `this.options.headers` (the spec-load credentials) to third-party
2320
+ // `$ref` hosts — that would leak the spec's auth token cross-origin.
2321
+ read: async (file) => {
2322
+ const response = await safeFetch(file.url, {
2323
+ timeoutMs: this.options.timeout,
2324
+ followRedirects: false,
2325
+ ssrf: refOpts
2326
+ });
2327
+ if (!response.ok) {
2328
+ throw new LoadError(
2329
+ `Failed to resolve external $ref "${file.url}": ${response.status} ${response.statusText}`,
2330
+ { url: file.url, status: response.status }
2331
+ );
2332
+ }
2333
+ return response.text();
2334
+ }
2335
+ };
2336
+ } else {
2337
+ resolveConfig["http"] = false;
2338
+ }
2339
+ return { resolve: resolveConfig };
1631
2340
  }
1632
2341
  /**
1633
- * Add pattern to schema
2342
+ * Does the document contain any EXTERNAL `$ref` (a ref that is not a local
2343
+ * JSON-pointer beginning with `#`)? Only external refs require the full
2344
+ * `$RefParser` (file/http resolvers, which pull Node builtins). A document
2345
+ * with only internal refs can be dereferenced with the runtime-agnostic
2346
+ * resolver below — so it works on V8 isolates (Cloudflare Workers) too.
1634
2347
  */
1635
- static withPattern(schema, pattern) {
1636
- return {
1637
- ...schema,
1638
- pattern
1639
- };
2348
+ static hasExternalRefs(node, seen = /* @__PURE__ */ new Set()) {
2349
+ if (node === null || typeof node !== "object") return false;
2350
+ if (seen.has(node)) return false;
2351
+ seen.add(node);
2352
+ if (Array.isArray(node)) return node.some((n) => _OpenAPIToolGenerator.hasExternalRefs(n, seen));
2353
+ const ref = node.$ref;
2354
+ if (typeof ref === "string" && !ref.startsWith("#")) return true;
2355
+ return Object.values(node).some(
2356
+ (v) => _OpenAPIToolGenerator.hasExternalRefs(v, seen)
2357
+ );
1640
2358
  }
1641
2359
  /**
1642
- * Add enum to schema
2360
+ * Dereference local (`#/...`) `$ref`s without `$RefParser` — pure, dependency-
2361
+ * free, runtime-agnostic. A pointer cache makes circular schemas resolve to a
2362
+ * shared reference instead of recursing forever (same contract as `$RefParser`).
1643
2363
  */
1644
- static withEnum(schema, values) {
1645
- return {
1646
- ...schema,
1647
- enum: values
2364
+ static dereferenceInternal(root) {
2365
+ const cache = /* @__PURE__ */ new Map();
2366
+ const resolvePointer = (ptr) => {
2367
+ const parts = ptr.replace(/^#\/?/, "").split("/").filter((p) => p.length > 0).map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
2368
+ let cur = root;
2369
+ for (const p of parts) cur = cur?.[p];
2370
+ return cur;
2371
+ };
2372
+ const walk = (node) => {
2373
+ if (node === null || typeof node !== "object") return node;
2374
+ if (Array.isArray(node)) return node.map(walk);
2375
+ const ref = node.$ref;
2376
+ if (typeof ref === "string" && ref.startsWith("#")) {
2377
+ const cached = cache.get(ref);
2378
+ if (cached !== void 0) return cached;
2379
+ const placeholder = {};
2380
+ cache.set(ref, placeholder);
2381
+ const resolved = walk(resolvePointer(ref));
2382
+ if (resolved && typeof resolved === "object") Object.assign(placeholder, resolved);
2383
+ return placeholder;
2384
+ }
2385
+ const out = {};
2386
+ for (const [k, v] of Object.entries(node)) out[k] = walk(v);
2387
+ return out;
1648
2388
  };
2389
+ return walk(root);
1649
2390
  }
1650
2391
  /**
1651
- * Add minimum/maximum constraints
2392
+ * Initialize the generator (dereference if needed, then validate)
1652
2393
  */
1653
- static withRange(schema, min, max, options = {}) {
1654
- const result = { ...schema };
1655
- if (min !== void 0) {
1656
- if (options.exclusive) {
1657
- result.exclusiveMinimum = min;
2394
+ async initialize() {
2395
+ if (this.options.dereference && !this.dereferencedDocument) {
2396
+ const cloned = JSON.parse(JSON.stringify(this.document));
2397
+ if (!_OpenAPIToolGenerator.hasExternalRefs(cloned)) {
2398
+ this.dereferencedDocument = _OpenAPIToolGenerator.dereferenceInternal(cloned);
1658
2399
  } else {
1659
- result.minimum = min;
2400
+ try {
2401
+ const { default: $RefParser } = await import("@apidevtools/json-schema-ref-parser");
2402
+ const refParserOptions = this.buildRefParserOptions();
2403
+ this.dereferencedDocument = await $RefParser.dereference(cloned, refParserOptions);
2404
+ } catch (error) {
2405
+ const errorMessage = error instanceof Error ? error.message : String(error);
2406
+ throw new ParseError(`Failed to dereference OpenAPI document: ${errorMessage}`, {
2407
+ originalError: error
2408
+ });
2409
+ }
1660
2410
  }
1661
2411
  }
1662
- if (max !== void 0) {
1663
- if (options.exclusive) {
1664
- result.exclusiveMaximum = max;
1665
- } else {
1666
- result.maximum = max;
2412
+ if (this.options.validate) {
2413
+ const validator = new Validator();
2414
+ const documentToValidate = this.dereferencedDocument ?? this.document;
2415
+ const result = await validator.validate(documentToValidate);
2416
+ if (!result.valid) {
2417
+ throw new ParseError("Invalid OpenAPI document", { errors: result.errors });
1667
2418
  }
1668
2419
  }
1669
- return result;
1670
2420
  }
1671
2421
  /**
1672
- * Add minLength/maxLength constraints
2422
+ * Generate all tools from the OpenAPI specification
1673
2423
  */
1674
- static withLength(schema, minLength, maxLength) {
1675
- const result = { ...schema };
1676
- if (minLength !== void 0) {
1677
- result.minLength = minLength;
2424
+ async generateTools(options = {}) {
2425
+ await this.initialize();
2426
+ const document = this.getDocument();
2427
+ const tools = [];
2428
+ const usedNames = /* @__PURE__ */ new Set();
2429
+ if (!document.paths) {
2430
+ return tools;
1678
2431
  }
1679
- if (maxLength !== void 0) {
1680
- result.maxLength = maxLength;
2432
+ const sortedPaths = Object.entries(document.paths).sort(([a], [b]) => a < b ? -1 : 1);
2433
+ for (const [pathStr, pathItem] of sortedPaths) {
2434
+ if (!pathItem || "$ref" in pathItem) continue;
2435
+ const methods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
2436
+ for (const method of methods) {
2437
+ const operation = pathItem[method];
2438
+ if (!operation) continue;
2439
+ if (!this.shouldIncludeOperation(operation, pathStr, method, options, document, pathItem)) {
2440
+ continue;
2441
+ }
2442
+ try {
2443
+ let tool = await this.generateTool(pathStr, method, options);
2444
+ if (usedNames.has(tool.name)) {
2445
+ const maxLength = options.maxToolNameLength ?? DEFAULT_MAX_TOOL_NAME_LENGTH;
2446
+ let seed = `${method} ${pathStr}`;
2447
+ let deduped = normalizeToolName(`${tool.name}_${fnv1aHex(seed)}`, maxLength, seed);
2448
+ let attempts = 1;
2449
+ while (usedNames.has(deduped)) {
2450
+ if (attempts >= MAX_NAME_DEDUP_ATTEMPTS) {
2451
+ throw new GenerationError(
2452
+ `Unable to find a unique tool name for "${tool.name}" (${method.toUpperCase()} ${pathStr}) within ${MAX_NAME_DEDUP_ATTEMPTS} attempts \u2014 the name space under maxToolNameLength=${maxLength} is exhausted. Increase maxToolNameLength or rename the operation.`,
2453
+ { name: tool.name, method, path: pathStr, maxToolNameLength: maxLength }
2454
+ );
2455
+ }
2456
+ seed += "#";
2457
+ deduped = normalizeToolName(`${tool.name}_${fnv1aHex(seed)}`, maxLength, seed);
2458
+ attempts++;
2459
+ }
2460
+ tool = { ...tool, name: deduped };
2461
+ }
2462
+ usedNames.add(tool.name);
2463
+ tools.push(tool);
2464
+ } catch (error) {
2465
+ const errorMessage = error instanceof Error ? error.message : String(error);
2466
+ console.warn(`Failed to generate tool for ${method.toUpperCase()} ${pathStr}:`, errorMessage);
2467
+ }
2468
+ }
1681
2469
  }
1682
- return result;
1683
- }
1684
- /**
1685
- * Create object schema
1686
- */
1687
- static object(properties, required) {
1688
- return {
1689
- type: "object",
1690
- properties,
1691
- ...required && required.length > 0 && { required },
1692
- additionalProperties: false
1693
- };
1694
- }
1695
- /**
1696
- * Create array schema
1697
- */
1698
- static array(items, constraints) {
1699
- return {
1700
- type: "array",
1701
- items,
1702
- ...constraints
1703
- };
2470
+ return tools;
1704
2471
  }
1705
2472
  /**
1706
- * Create string schema
2473
+ * Generate a specific tool for a path and method
1707
2474
  */
1708
- static string(constraints) {
1709
- return {
1710
- type: "string",
1711
- ...constraints
2475
+ async generateTool(pathStr, method, options = {}) {
2476
+ await this.initialize();
2477
+ const document = this.getDocument();
2478
+ if (!document.paths) {
2479
+ throw new Error("No paths defined in OpenAPI document");
2480
+ }
2481
+ const pathItem = document.paths[pathStr];
2482
+ const operation = pathItem?.[method.toLowerCase()];
2483
+ if (!operation) {
2484
+ throw new Error(`Operation not found: ${method.toUpperCase()} ${pathStr}`);
2485
+ }
2486
+ const parameterResolver = new ParameterResolver(options.namingStrategy, {
2487
+ includeExamples: options.includeExamples
2488
+ });
2489
+ let pathParameters = void 0;
2490
+ if (pathItem.parameters) {
2491
+ pathParameters = pathItem.parameters.filter(
2492
+ (p) => !isReferenceObject(p)
2493
+ );
2494
+ }
2495
+ let securityRequirements = void 0;
2496
+ const securitySpec = operation.security ?? document.security;
2497
+ if (securitySpec) {
2498
+ securityRequirements = this.extractSecurityRequirements(securitySpec, document);
2499
+ }
2500
+ const { inputSchema, mapper } = parameterResolver.resolve(
2501
+ operation,
2502
+ pathParameters,
2503
+ securityRequirements,
2504
+ options.includeSecurityInInput
2505
+ );
2506
+ const responseBuilder = new ResponseBuilder(options);
2507
+ const outputSchema = responseBuilder.build(operation.responses);
2508
+ const overrides = extractExtensionOverrides(operation);
2509
+ const name = this.generateToolName(pathStr, method, overrides.name ?? operation.operationId, options);
2510
+ const description = overrides.description ?? (operation.summary || operation.description || `${method.toUpperCase()} ${pathStr}`);
2511
+ const title = overrides.title ?? operation.summary;
2512
+ const inferred = options.inferAnnotations !== false ? inferAnnotationsFromMethod(method.toLowerCase()) : void 0;
2513
+ const annotations = inferred || overrides.annotations ? { ...inferred, ...overrides.annotations } : void 0;
2514
+ const metadata = this.extractMetadata(pathStr, method, operation, document, outputSchema);
2515
+ const formatResolvers = {
2516
+ ...options.resolveFormats ? BUILTIN_FORMAT_RESOLVERS : {},
2517
+ ...options.formatResolvers
1712
2518
  };
1713
- }
1714
- /**
1715
- * Create number schema
1716
- */
1717
- static number(constraints) {
2519
+ const hasFormatResolvers = Object.keys(formatResolvers).length > 0;
2520
+ let resolvedInputSchema = hasFormatResolvers ? resolveSchemaFormats(inputSchema, formatResolvers) : inputSchema;
2521
+ let resolvedOutputSchema = hasFormatResolvers && outputSchema ? resolveSchemaFormats(outputSchema, formatResolvers) : outputSchema;
2522
+ const maxSchemaDepth = Math.max(1, options.maxSchemaDepth ?? 10);
2523
+ resolvedInputSchema = SchemaBuilder.truncateDepth(resolvedInputSchema, maxSchemaDepth);
2524
+ if (resolvedOutputSchema) {
2525
+ resolvedOutputSchema = SchemaBuilder.truncateDepth(resolvedOutputSchema, maxSchemaDepth);
2526
+ }
2527
+ if (options.target) {
2528
+ resolvedInputSchema = applyClientTarget(resolvedInputSchema, options.target);
2529
+ if (resolvedOutputSchema) {
2530
+ resolvedOutputSchema = applyClientTarget(resolvedOutputSchema, options.target);
2531
+ }
2532
+ }
1718
2533
  return {
1719
- type: "number",
1720
- ...constraints
2534
+ name,
2535
+ ...title !== void 0 && { title },
2536
+ description,
2537
+ ...annotations && { annotations },
2538
+ inputSchema: resolvedInputSchema,
2539
+ outputSchema: resolvedOutputSchema,
2540
+ mapper,
2541
+ metadata
1721
2542
  };
1722
2543
  }
1723
2544
  /**
1724
- * Create integer schema
2545
+ * Check if an operation should be included
1725
2546
  */
1726
- static integer(constraints) {
1727
- return {
1728
- type: "integer",
1729
- ...constraints
1730
- };
2547
+ shouldIncludeOperation(operation, path, method, options, document, pathItem) {
2548
+ if (!resolveExtensionEnabled(document, pathItem, operation)) {
2549
+ return false;
2550
+ }
2551
+ if (operation.deprecated && !options.includeDeprecated) {
2552
+ return false;
2553
+ }
2554
+ const lowerMethod = method.toLowerCase();
2555
+ if (options.includeMethods && !options.includeMethods.includes(lowerMethod)) {
2556
+ return false;
2557
+ }
2558
+ if (options.excludeMethods?.includes(lowerMethod)) {
2559
+ return false;
2560
+ }
2561
+ if (options.includePaths && !matchesAnyGlob(path, options.includePaths)) {
2562
+ return false;
2563
+ }
2564
+ if (options.excludePaths && matchesAnyGlob(path, options.excludePaths)) {
2565
+ return false;
2566
+ }
2567
+ const tags = operation.tags ?? [];
2568
+ if (options.includeTags && !tags.some((tag) => options.includeTags.includes(tag))) {
2569
+ return false;
2570
+ }
2571
+ if (options.excludeTags && tags.some((tag) => options.excludeTags.includes(tag))) {
2572
+ return false;
2573
+ }
2574
+ if (options.includeOperations && operation.operationId) {
2575
+ if (!options.includeOperations.includes(operation.operationId)) {
2576
+ return false;
2577
+ }
2578
+ }
2579
+ if (options.excludeOperations && operation.operationId) {
2580
+ if (options.excludeOperations.includes(operation.operationId)) {
2581
+ return false;
2582
+ }
2583
+ }
2584
+ if (options.readOnlyOnly) {
2585
+ const effective = {
2586
+ ...inferAnnotationsFromMethod(lowerMethod),
2587
+ ...extractExtensionOverrides(operation).annotations
2588
+ };
2589
+ if (effective.readOnlyHint !== true) {
2590
+ return false;
2591
+ }
2592
+ }
2593
+ if (options.filterFn) {
2594
+ return options.filterFn({
2595
+ ...operation,
2596
+ path,
2597
+ method
2598
+ });
2599
+ }
2600
+ return true;
1731
2601
  }
1732
2602
  /**
1733
- * Create boolean schema
2603
+ * Generate a tool name
1734
2604
  */
1735
- static boolean() {
1736
- return {
1737
- type: "boolean"
1738
- };
2605
+ generateToolName(path, method, operationId, options = {}) {
2606
+ let rawName;
2607
+ if (options.namingStrategy?.toolNameGenerator) {
2608
+ rawName = options.namingStrategy.toolNameGenerator(path, method, operationId);
2609
+ } else if (operationId) {
2610
+ rawName = operationId;
2611
+ } else {
2612
+ const sanitized = path.replace(/\{([^}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
2613
+ rawName = `${method}_${sanitized}`;
2614
+ }
2615
+ return normalizeToolName(
2616
+ rawName,
2617
+ options.maxToolNameLength ?? DEFAULT_MAX_TOOL_NAME_LENGTH,
2618
+ `${method} ${path}`
2619
+ );
1739
2620
  }
1740
2621
  /**
1741
- * Create null schema
2622
+ * Extract metadata from operation
1742
2623
  */
1743
- static null() {
1744
- return {
1745
- type: "null"
2624
+ extractMetadata(path, method, operation, document, outputSchema) {
2625
+ const metadata = {
2626
+ path,
2627
+ method,
2628
+ operationId: operation.operationId,
2629
+ operationSummary: operation.summary,
2630
+ operationDescription: operation.description,
2631
+ tags: operation.tags,
2632
+ deprecated: operation.deprecated
1746
2633
  };
1747
- }
1748
- /**
1749
- * Flatten nested oneOf/anyOf/allOf schemas
1750
- */
1751
- static flatten(schema, maxDepth = 10) {
1752
- if (maxDepth <= 0) return schema;
1753
- const cloned = this.clone(schema);
1754
- if (cloned.oneOf) {
1755
- const flattened = cloned.oneOf.flatMap((s) => {
1756
- const sub = this.flatten(s, maxDepth - 1);
1757
- return sub.oneOf ? sub.oneOf : [sub];
1758
- });
1759
- cloned.oneOf = flattened;
2634
+ if (operation.security || document.security) {
2635
+ metadata.security = this.extractSecurityRequirements(
2636
+ operation.security ?? document.security,
2637
+ document
2638
+ );
1760
2639
  }
1761
- if (cloned.anyOf) {
1762
- const flattened = cloned.anyOf.flatMap((s) => {
1763
- const sub = this.flatten(s, maxDepth - 1);
1764
- return sub.anyOf ? sub.anyOf : [sub];
1765
- });
1766
- cloned.anyOf = flattened;
2640
+ const servers = operation.servers ?? document.servers;
2641
+ if (servers) {
2642
+ metadata.servers = servers.map((server) => ({
2643
+ url: this.options.baseUrl || server.url,
2644
+ description: server.description,
2645
+ variables: server.variables
2646
+ }));
2647
+ } else if (this.options.baseUrl) {
2648
+ metadata.servers = [{ url: this.options.baseUrl }];
1767
2649
  }
1768
- if (cloned.allOf) {
1769
- const flattened = cloned.allOf.flatMap((s) => {
1770
- const sub = this.flatten(s, maxDepth - 1);
1771
- return sub.allOf ? sub.allOf : [sub];
1772
- });
1773
- cloned.allOf = flattened;
2650
+ const schemaObj = outputSchema;
2651
+ if (schemaObj && Array.isArray(schemaObj["oneOf"])) {
2652
+ const codes = schemaObj["oneOf"].map((schema) => schema["x-status-code"]).filter((code) => code !== void 0 && code !== null);
2653
+ if (codes.length > 0) {
2654
+ metadata.responseStatusCodes = codes;
2655
+ }
2656
+ } else if (schemaObj && schemaObj["x-status-code"] !== void 0 && schemaObj["x-status-code"] !== null) {
2657
+ metadata.responseStatusCodes = [schemaObj["x-status-code"]];
1774
2658
  }
1775
- return cloned;
2659
+ if (operation.externalDocs) {
2660
+ metadata.externalDocs = operation.externalDocs;
2661
+ }
2662
+ const operationWithExt = operation;
2663
+ if (operationWithExt["x-frontmcp"]) {
2664
+ metadata.frontmcp = operationWithExt["x-frontmcp"];
2665
+ }
2666
+ return metadata;
1776
2667
  }
1777
2668
  /**
1778
- * Simplify schema by removing unnecessary fields
2669
+ * Extract security requirements
1779
2670
  */
1780
- static simplify(schema) {
1781
- const cloned = this.clone(schema);
1782
- if (Array.isArray(cloned.required) && cloned.required.length === 0) {
1783
- delete cloned.required;
1784
- }
1785
- if (cloned.properties && Object.keys(cloned.properties).length === 0) {
1786
- delete cloned.properties;
1787
- }
1788
- if (Array.isArray(cloned.examples) && cloned.examples.length === 0) {
1789
- delete cloned.examples;
1790
- }
1791
- if (cloned.title && cloned.description && cloned.title === cloned.description) {
1792
- delete cloned.title;
2671
+ extractSecurityRequirements(security, document) {
2672
+ if (!security || !document.components?.securitySchemes) {
2673
+ return [];
1793
2674
  }
1794
- return cloned;
2675
+ return security.flatMap(
2676
+ (req) => Object.entries(req).map(([scheme, scopes]) => {
2677
+ const securityScheme = document.components.securitySchemes[scheme];
2678
+ if (isReferenceObject(securityScheme)) {
2679
+ return { scheme, type: "http", scopes };
2680
+ }
2681
+ const apiKeyIn = "in" in securityScheme ? securityScheme.in : void 0;
2682
+ const result = {
2683
+ scheme,
2684
+ type: securityScheme.type,
2685
+ scopes,
2686
+ name: "name" in securityScheme ? securityScheme.name : void 0,
2687
+ in: apiKeyIn && (apiKeyIn === "query" || apiKeyIn === "header" || apiKeyIn === "cookie") ? apiKeyIn : void 0
2688
+ };
2689
+ if (securityScheme.type === "http") {
2690
+ result.httpScheme = "scheme" in securityScheme ? securityScheme.scheme : void 0;
2691
+ result.bearerFormat = "bearerFormat" in securityScheme ? securityScheme.bearerFormat : void 0;
2692
+ }
2693
+ result.description = "description" in securityScheme ? securityScheme.description : void 0;
2694
+ return result;
2695
+ })
2696
+ );
1795
2697
  }
1796
2698
  };
1797
2699
 
@@ -2038,6 +2940,351 @@ function createSecurityContext(auth) {
2038
2940
  customResolver: auth.customResolver
2039
2941
  };
2040
2942
  }
2943
+
2944
+ // src/request-builder.ts
2945
+ var RESERVED_DECODE = {
2946
+ "%3A": ":",
2947
+ "%2F": "/",
2948
+ "%3F": "?",
2949
+ "%23": "#",
2950
+ "%5B": "[",
2951
+ "%5D": "]",
2952
+ "%40": "@",
2953
+ "%24": "$",
2954
+ "%26": "&",
2955
+ "%2B": "+",
2956
+ "%2C": ",",
2957
+ "%3B": ";",
2958
+ "%3D": "="
2959
+ };
2960
+ function encodeValue(value, allowReserved) {
2961
+ const encoded = encodeURIComponent(value);
2962
+ if (!allowReserved) return encoded;
2963
+ return encoded.replace(/%3A|%2F|%3F|%23|%5B|%5D|%40|%24|%26|%2B|%2C|%3B|%3D/gi, (m) => RESERVED_DECODE[m.toUpperCase()]);
2964
+ }
2965
+ function isPlainObject(value) {
2966
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2967
+ }
2968
+ function primitiveString(value, paramName, location) {
2969
+ if (value === null || value === void 0 || typeof value === "object") {
2970
+ throw new RequestBuildError(
2971
+ `${location} parameter '${paramName}' must serialize to a primitive; received ${value === null ? "null" : Array.isArray(value) ? "an array" : typeof value}`,
2972
+ { param: paramName, location }
2973
+ );
2974
+ }
2975
+ return String(value);
2976
+ }
2977
+ function serializePathValue(mapper, value) {
2978
+ const style = mapper.style ?? "simple";
2979
+ const explode = mapper.explode ?? false;
2980
+ const name = mapper.key;
2981
+ const enc = (v) => encodeValue(primitiveString(v, name, "path"));
2982
+ if (Array.isArray(value)) {
2983
+ if (style === "label") {
2984
+ return `.${value.map(enc).join(explode ? "." : ",")}`;
2985
+ }
2986
+ if (style === "matrix") {
2987
+ return explode ? value.map((v) => `;${name}=${enc(v)}`).join("") : `;${name}=${value.map(enc).join(",")}`;
2988
+ }
2989
+ return value.map(enc).join(",");
2990
+ }
2991
+ if (isPlainObject(value)) {
2992
+ const entries = Object.entries(value);
2993
+ if (style === "label") {
2994
+ return explode ? entries.map(([k, v]) => `.${encodeValue(k)}=${enc(v)}`).join("") : `.${entries.map(([k, v]) => `${encodeValue(k)},${enc(v)}`).join(",")}`;
2995
+ }
2996
+ if (style === "matrix") {
2997
+ return explode ? entries.map(([k, v]) => `;${encodeValue(k)}=${enc(v)}`).join("") : `;${name}=${entries.map(([k, v]) => `${encodeValue(k)},${enc(v)}`).join(",")}`;
2998
+ }
2999
+ return explode ? entries.map(([k, v]) => `${encodeValue(k)}=${enc(v)}`).join(",") : entries.map(([k, v]) => `${encodeValue(k)},${enc(v)}`).join(",");
3000
+ }
3001
+ const core = enc(value);
3002
+ if (style === "label") return `.${core}`;
3003
+ if (style === "matrix") return `;${name}=${core}`;
3004
+ return core;
3005
+ }
3006
+ function serializeQueryPairs(mapper, value) {
3007
+ const style = mapper.style ?? "form";
3008
+ const explode = mapper.explode ?? style === "form";
3009
+ const name = mapper.key;
3010
+ const str = (v) => primitiveString(v, name, "query");
3011
+ if (Array.isArray(value)) {
3012
+ if ((style === "deepObject" ? mapper.explode ?? true : explode) || value.length === 0) {
3013
+ return value.map((v) => [name, str(v)]);
3014
+ }
3015
+ const delimiter = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ",";
3016
+ return [[name, value.map(str).join(delimiter)]];
3017
+ }
3018
+ if (isPlainObject(value)) {
3019
+ if (style === "deepObject") {
3020
+ const pairs = [];
3021
+ const walk = (prefix, node) => {
3022
+ for (const [k, v] of Object.entries(node)) {
3023
+ if (v === void 0) continue;
3024
+ if (isPlainObject(v)) {
3025
+ walk(`${prefix}[${k}]`, v);
3026
+ } else if (Array.isArray(v)) {
3027
+ for (const item of v) pairs.push([`${prefix}[${k}]`, str(item)]);
3028
+ } else {
3029
+ pairs.push([`${prefix}[${k}]`, str(v)]);
3030
+ }
3031
+ }
3032
+ };
3033
+ walk(name, value);
3034
+ return pairs;
3035
+ }
3036
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0);
3037
+ if (explode) {
3038
+ return entries.map(([k, v]) => [k, str(v)]);
3039
+ }
3040
+ return [[name, entries.map(([k, v]) => `${k},${str(v)}`).join(",")]];
3041
+ }
3042
+ return [[name, str(value)]];
3043
+ }
3044
+ function serializeHeaderValue(mapper, value) {
3045
+ const explode = mapper.explode ?? false;
3046
+ const name = mapper.key;
3047
+ const str = (v) => primitiveString(v, name, "header");
3048
+ if (Array.isArray(value)) {
3049
+ return value.map(str).join(",");
3050
+ }
3051
+ if (isPlainObject(value)) {
3052
+ const entries = Object.entries(value);
3053
+ return explode ? entries.map(([k, v]) => `${k}=${str(v)}`).join(",") : entries.map(([k, v]) => `${k},${str(v)}`).join(",");
3054
+ }
3055
+ return str(value);
3056
+ }
3057
+ function assertHeaderSafe(name, value) {
3058
+ if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
3059
+ throw new RequestBuildError(`Invalid header name '${name}' (RFC 7230 token required)`, { header: name });
3060
+ }
3061
+ if (/[\r\n\x00]/.test(value)) {
3062
+ throw new RequestBuildError(`Header '${name}' value contains control characters (possible header injection)`, {
3063
+ header: name
3064
+ });
3065
+ }
3066
+ }
3067
+ function assertCookieName(name) {
3068
+ if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
3069
+ throw new RequestBuildError(`Invalid cookie name '${name}' (RFC 6265 token required)`, { cookie: name });
3070
+ }
3071
+ }
3072
+ function assertCookieValue(name, value) {
3073
+ if (/[\x00-\x1f\x7f\s";\\]/.test(value)) {
3074
+ throw new RequestBuildError(
3075
+ `Cookie '${name}' value contains characters that break the Cookie header (RFC 6265 cookie-octet violation)`,
3076
+ { cookie: name }
3077
+ );
3078
+ }
3079
+ }
3080
+ function formatSecurityValue(mapper, value) {
3081
+ const security = mapper.security;
3082
+ if (security.type === "http") {
3083
+ const scheme = (security.httpScheme ?? "bearer").toLowerCase();
3084
+ if (scheme !== "bearer" && scheme !== "basic") {
3085
+ return value;
3086
+ }
3087
+ const prefix = scheme.charAt(0).toUpperCase() + scheme.slice(1);
3088
+ return value.toLowerCase().startsWith(`${scheme} `) ? value : `${prefix} ${value}`;
3089
+ }
3090
+ if (security.type === "oauth2" || security.type === "openIdConnect") {
3091
+ return value.toLowerCase().startsWith("bearer ") ? value : `Bearer ${value}`;
3092
+ }
3093
+ return value;
3094
+ }
3095
+ function resolveServerUrl(tool) {
3096
+ const server = tool.metadata.servers?.[0];
3097
+ if (!server) return "";
3098
+ let url = server.url;
3099
+ if (server.variables) {
3100
+ for (const [name, variable] of Object.entries(server.variables)) {
3101
+ if (variable && typeof variable.default === "string") {
3102
+ url = url.replaceAll(`{${name}}`, variable.default);
3103
+ }
3104
+ }
3105
+ }
3106
+ return url;
3107
+ }
3108
+ var JSON_CONTENT = /^application\/(.+\+)?json$/i;
3109
+ function buildHttpRequest(tool, input, options = {}) {
3110
+ const rawBase = options.baseUrl ?? resolveServerUrl(tool);
3111
+ if (rawBase.includes("{")) {
3112
+ throw new RequestBuildError(
3113
+ `Base URL '${rawBase}' contains unresolved server template variables (no default value in the spec); pass an explicit baseUrl`,
3114
+ { baseUrl: rawBase }
3115
+ );
3116
+ }
3117
+ if (rawBase !== "" && !/^https?:\/\//i.test(rawBase)) {
3118
+ throw new RequestBuildError(`Base URL must be http(s) or empty; received '${rawBase}'`, { baseUrl: rawBase });
3119
+ }
3120
+ let base = rawBase;
3121
+ while (base.endsWith("/")) base = base.slice(0, -1);
3122
+ let path = tool.metadata.path;
3123
+ const queryPairs = [];
3124
+ const query = {};
3125
+ const headers = {};
3126
+ const cookies = {};
3127
+ let rawBody;
3128
+ let bodyObject;
3129
+ let contentType;
3130
+ let hasBody = false;
3131
+ let binaryBody = false;
3132
+ for (const mapper of tool.mapper) {
3133
+ const value = input[mapper.inputKey];
3134
+ if (mapper.security) {
3135
+ if (value === void 0 || value === null) continue;
3136
+ const formatted = formatSecurityValue(mapper, String(value));
3137
+ if (mapper.type === "header") {
3138
+ assertHeaderSafe(mapper.key, formatted);
3139
+ headers[mapper.key] = formatted;
3140
+ } else if (mapper.type === "query") {
3141
+ queryPairs.push([mapper.key, formatted]);
3142
+ } else {
3143
+ assertCookieName(mapper.key);
3144
+ cookies[mapper.key] = formatted;
3145
+ }
3146
+ continue;
3147
+ }
3148
+ if (value === void 0 || value === null && mapper.type !== "body") {
3149
+ if (mapper.required) {
3150
+ throw new RequestBuildError(
3151
+ `Required ${mapper.type} parameter '${mapper.key}' (input key '${mapper.inputKey}') is missing`,
3152
+ { param: mapper.key, inputKey: mapper.inputKey, location: mapper.type }
3153
+ );
3154
+ }
3155
+ continue;
3156
+ }
3157
+ switch (mapper.type) {
3158
+ case "path":
3159
+ path = path.replaceAll(`{${mapper.key}}`, serializePathValue(mapper, value));
3160
+ break;
3161
+ case "query":
3162
+ for (const [k, v] of serializeQueryPairs(mapper, value)) {
3163
+ queryPairs.push([k, v, mapper.allowReserved]);
3164
+ }
3165
+ break;
3166
+ case "header": {
3167
+ const headerValue = serializeHeaderValue(mapper, value);
3168
+ assertHeaderSafe(mapper.key, headerValue);
3169
+ headers[mapper.key] = headerValue;
3170
+ break;
3171
+ }
3172
+ case "cookie": {
3173
+ assertCookieName(mapper.key);
3174
+ cookies[mapper.key] = Array.isArray(value) ? value.map((v) => primitiveString(v, mapper.key, "cookie")).join(",") : primitiveString(value, mapper.key, "cookie");
3175
+ break;
3176
+ }
3177
+ case "body":
3178
+ hasBody = true;
3179
+ contentType = contentType ?? mapper.serialization?.contentType ?? "application/json";
3180
+ if (mapper.serialization?.binary) binaryBody = true;
3181
+ if (mapper.wholeBody) {
3182
+ rawBody = value;
3183
+ } else {
3184
+ if (bodyObject === void 0) bodyObject = {};
3185
+ bodyObject[mapper.key] = value;
3186
+ }
3187
+ break;
3188
+ }
3189
+ }
3190
+ if (path.includes("{")) {
3191
+ throw new RequestBuildError(`Unresolved path parameters remain in '${path}'`, { path });
3192
+ }
3193
+ if (bodyObject !== void 0) rawBody = bodyObject;
3194
+ const queryString = queryPairs.map(([k, v, allowReserved]) => {
3195
+ query[k] = query[k] ?? [];
3196
+ query[k].push(v);
3197
+ const encodedKey = encodeURIComponent(k).replace(/%5B/gi, "[").replace(/%5D/gi, "]");
3198
+ return `${encodedKey}=${encodeValue(v, allowReserved)}`;
3199
+ }).join("&");
3200
+ const cookieEntries = Object.entries(cookies);
3201
+ if (cookieEntries.length > 0) {
3202
+ for (const [k, v] of cookieEntries) assertCookieValue(k, v);
3203
+ headers["Cookie"] = cookieEntries.map(([k, v]) => `${k}=${v}`).join("; ");
3204
+ }
3205
+ const contentTypeKey = Object.keys(headers).find((h) => h.toLowerCase() === "content-type") ?? "content-type";
3206
+ const hasExplicitContentType = contentTypeKey in headers;
3207
+ let body;
3208
+ if (hasBody && rawBody !== void 0) {
3209
+ const ct = contentType;
3210
+ if (binaryBody) {
3211
+ body = rawBody;
3212
+ if (!hasExplicitContentType) headers[contentTypeKey] = ct;
3213
+ } else if (ct.toLowerCase() === "application/x-www-form-urlencoded") {
3214
+ const params = new URLSearchParams();
3215
+ if (!isPlainObject(rawBody)) {
3216
+ throw new RequestBuildError(`form-urlencoded bodies must be objects; received ${typeof rawBody}`, {
3217
+ contentType: ct
3218
+ });
3219
+ }
3220
+ for (const [k, v] of Object.entries(rawBody)) {
3221
+ if (v === void 0) continue;
3222
+ if (Array.isArray(v)) {
3223
+ for (const item of v) params.append(k, primitiveString(item, k, "body"));
3224
+ } else {
3225
+ params.append(k, isPlainObject(v) ? JSON.stringify(v) : String(v));
3226
+ }
3227
+ }
3228
+ body = params.toString();
3229
+ headers[contentTypeKey] = ct;
3230
+ } else if (ct.toLowerCase() === "multipart/form-data") {
3231
+ if (typeof FormData === "undefined") {
3232
+ throw new RequestBuildError("multipart/form-data requires a FormData implementation in this runtime", {});
3233
+ }
3234
+ const form = new FormData();
3235
+ if (!isPlainObject(rawBody)) {
3236
+ throw new RequestBuildError(`multipart bodies must be objects; received ${typeof rawBody}`, {
3237
+ contentType: ct
3238
+ });
3239
+ }
3240
+ for (const [k, v] of Object.entries(rawBody)) {
3241
+ if (v === void 0) continue;
3242
+ if (typeof Blob !== "undefined" && v instanceof Blob) {
3243
+ form.append(k, v);
3244
+ } else if (v instanceof Uint8Array) {
3245
+ form.append(k, new Blob([v]));
3246
+ } else if (isPlainObject(v) || Array.isArray(v)) {
3247
+ form.append(k, JSON.stringify(v));
3248
+ } else {
3249
+ form.append(k, String(v));
3250
+ }
3251
+ }
3252
+ body = form;
3253
+ if (hasExplicitContentType) delete headers[contentTypeKey];
3254
+ } else if (JSON_CONTENT.test(ct)) {
3255
+ body = JSON.stringify(rawBody);
3256
+ headers[contentTypeKey] = ct;
3257
+ } else {
3258
+ body = isPlainObject(rawBody) || Array.isArray(rawBody) ? JSON.stringify(rawBody) : String(rawBody);
3259
+ headers[contentTypeKey] = ct;
3260
+ }
3261
+ }
3262
+ return {
3263
+ url: `${base}${path}${queryString ? `?${queryString}` : ""}`,
3264
+ method: tool.metadata.method.toUpperCase(),
3265
+ headers,
3266
+ query,
3267
+ cookies,
3268
+ contentType,
3269
+ body,
3270
+ rawBody
3271
+ };
3272
+ }
3273
+
3274
+ // src/sdk.ts
3275
+ function toSdkTool(tool, wrapper) {
3276
+ const wrapSchema = wrapper?.fromJsonSchema ?? ((schema) => schema);
3277
+ return [
3278
+ tool.name,
3279
+ {
3280
+ ...tool.title !== void 0 && { title: tool.title },
3281
+ description: tool.description,
3282
+ inputSchema: wrapSchema(tool.inputSchema),
3283
+ ...tool.outputSchema !== void 0 && { outputSchema: wrapSchema(tool.outputSchema) },
3284
+ ...tool.annotations !== void 0 && { annotations: tool.annotations }
3285
+ }
3286
+ ];
3287
+ }
2041
3288
  // Annotate the CommonJS export names for ESM import in node:
2042
3289
  0 && (module.exports = {
2043
3290
  BLOCKED_HOSTNAMES,
@@ -2048,6 +3295,7 @@ function createSecurityContext(auth) {
2048
3295
  OpenAPIToolGenerator,
2049
3296
  ParameterResolver,
2050
3297
  ParseError,
3298
+ RequestBuildError,
2051
3299
  ResponseBuilder,
2052
3300
  SchemaBuilder,
2053
3301
  SchemaError,
@@ -2055,15 +3303,28 @@ function createSecurityContext(auth) {
2055
3303
  SsrfError,
2056
3304
  ValidationError,
2057
3305
  Validator,
3306
+ applyClientTarget,
2058
3307
  assertUrlSafe,
3308
+ buildHttpRequest,
3309
+ collapseNestedUnions,
3310
+ collapseRootCompositions,
2059
3311
  createSecurityContext,
2060
3312
  decodeIpv4MappedIpv6,
2061
3313
  defaultLookup,
3314
+ demoteFormats,
3315
+ enforceClosedObjects,
3316
+ ensureArrayItems,
3317
+ extractExtensionOverrides,
3318
+ inferAnnotationsFromMethod,
3319
+ inlineLocalRefs,
2062
3320
  isBlockedAddress,
2063
3321
  isBlockedHostname,
2064
3322
  isReferenceObject,
2065
3323
  normalizeSsrfOptions,
3324
+ requireAllProperties,
3325
+ resolveExtensionEnabled,
2066
3326
  resolveSchemaFormats,
2067
3327
  safeFetch,
2068
- toJsonSchema
3328
+ toJsonSchema,
3329
+ toSdkTool
2069
3330
  });