mcp-from-openapi 2.5.1 → 2.6.1

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
@@ -36,8 +36,10 @@ __export(index_exports, {
36
36
  LoadError: () => LoadError,
37
37
  OpenAPIToolError: () => OpenAPIToolError,
38
38
  OpenAPIToolGenerator: () => OpenAPIToolGenerator,
39
+ OverlayError: () => OverlayError,
39
40
  ParameterResolver: () => ParameterResolver,
40
41
  ParseError: () => ParseError,
42
+ RequestBuildError: () => RequestBuildError,
41
43
  ResponseBuilder: () => ResponseBuilder,
42
44
  SchemaBuilder: () => SchemaBuilder,
43
45
  SchemaError: () => SchemaError,
@@ -45,17 +47,34 @@ __export(index_exports, {
45
47
  SsrfError: () => SsrfError,
46
48
  ValidationError: () => ValidationError,
47
49
  Validator: () => Validator,
50
+ analyzeToolSet: () => analyzeToolSet,
51
+ applyClientTarget: () => applyClientTarget,
52
+ applyOverlay: () => applyOverlay,
48
53
  assertUrlSafe: () => assertUrlSafe,
54
+ buildHttpRequest: () => buildHttpRequest,
55
+ collapseNestedUnions: () => collapseNestedUnions,
56
+ collapseRootCompositions: () => collapseRootCompositions,
49
57
  createSecurityContext: () => createSecurityContext,
50
58
  decodeIpv4MappedIpv6: () => decodeIpv4MappedIpv6,
51
59
  defaultLookup: () => defaultLookup,
60
+ demoteFormats: () => demoteFormats,
61
+ enforceClosedObjects: () => enforceClosedObjects,
62
+ ensureArrayItems: () => ensureArrayItems,
63
+ estimateToolTokens: () => estimateToolTokens,
64
+ extractExtensionOverrides: () => extractExtensionOverrides,
65
+ inferAnnotationsFromMethod: () => inferAnnotationsFromMethod,
66
+ inlineLocalRefs: () => inlineLocalRefs,
52
67
  isBlockedAddress: () => isBlockedAddress,
53
68
  isBlockedHostname: () => isBlockedHostname,
54
69
  isReferenceObject: () => isReferenceObject,
70
+ lintDocument: () => lintDocument,
55
71
  normalizeSsrfOptions: () => normalizeSsrfOptions,
72
+ requireAllProperties: () => requireAllProperties,
73
+ resolveExtensionEnabled: () => resolveExtensionEnabled,
56
74
  resolveSchemaFormats: () => resolveSchemaFormats,
57
75
  safeFetch: () => safeFetch,
58
- toJsonSchema: () => toJsonSchema
76
+ toJsonSchema: () => toJsonSchema,
77
+ toSdkTool: () => toSdkTool
59
78
  });
60
79
  module.exports = __toCommonJS(index_exports);
61
80
 
@@ -71,7 +90,25 @@ function toJsonSchema(schema) {
71
90
  return { $ref: schema.$ref };
72
91
  }
73
92
  const { exclusiveMaximum, exclusiveMinimum, maximum, minimum, ...rest } = schema;
74
- const result = { ...rest };
93
+ const { nullable, example, ...cleanRest } = rest;
94
+ const result = { ...cleanRest };
95
+ delete result["xml"];
96
+ let wrapNullable = false;
97
+ if (nullable === true) {
98
+ const type = result["type"];
99
+ if (type === void 0) {
100
+ wrapNullable = true;
101
+ } else if (Array.isArray(type)) {
102
+ if (!type.includes("null")) {
103
+ result["type"] = [...type, "null"];
104
+ }
105
+ } else if (type !== "null") {
106
+ result["type"] = [type, "null"];
107
+ }
108
+ }
109
+ if (example !== void 0 && !Array.isArray(result["examples"])) {
110
+ result["examples"] = [example];
111
+ }
75
112
  if (typeof exclusiveMaximum === "boolean") {
76
113
  if (exclusiveMaximum && maximum !== void 0) {
77
114
  result["exclusiveMaximum"] = maximum;
@@ -125,16 +162,57 @@ function toJsonSchema(schema) {
125
162
  if (result["not"]) {
126
163
  result["not"] = toJsonSchema(result["not"]);
127
164
  }
165
+ for (const key of ["patternProperties", "$defs", "definitions", "dependentSchemas"]) {
166
+ const value = result[key];
167
+ if (value && typeof value === "object" && !Array.isArray(value)) {
168
+ const mapped = {};
169
+ for (const [name, sub] of Object.entries(value)) {
170
+ mapped[name] = toJsonSchema(sub);
171
+ }
172
+ result[key] = mapped;
173
+ }
174
+ }
175
+ for (const key of [
176
+ "contains",
177
+ "propertyNames",
178
+ "if",
179
+ "then",
180
+ "else",
181
+ "contentSchema",
182
+ "unevaluatedItems",
183
+ "unevaluatedProperties"
184
+ ]) {
185
+ const value = result[key];
186
+ if (value && typeof value === "object") {
187
+ result[key] = toJsonSchema(value);
188
+ }
189
+ }
190
+ if (Array.isArray(result["prefixItems"])) {
191
+ result["prefixItems"] = result["prefixItems"].map(toJsonSchema);
192
+ }
193
+ if (wrapNullable) {
194
+ const wrapper = {};
195
+ for (const key of ["title", "description", "deprecated", "examples"]) {
196
+ if (result[key] !== void 0) {
197
+ wrapper[key] = result[key];
198
+ delete result[key];
199
+ }
200
+ }
201
+ wrapper["anyOf"] = [result, { type: "null" }];
202
+ return wrapper;
203
+ }
128
204
  return result;
129
205
  }
130
206
 
131
207
  // src/parameter-resolver.ts
132
208
  var ParameterResolver = class {
133
209
  namingStrategy;
134
- constructor(namingStrategy) {
210
+ includeExamples;
211
+ constructor(namingStrategy, options) {
135
212
  this.namingStrategy = namingStrategy ?? {
136
213
  conflictResolver: this.defaultConflictResolver
137
214
  };
215
+ this.includeExamples = options?.includeExamples ?? false;
138
216
  }
139
217
  /**
140
218
  * Default conflict resolver: prefix with location
@@ -166,7 +244,8 @@ var ParameterResolver = class {
166
244
  style: param.style,
167
245
  explode: param.explode,
168
246
  allowReserved: param.allowReserved,
169
- deprecated: param.deprecated
247
+ deprecated: param.deprecated,
248
+ examples: this.includeExamples ? collectExampleValues(param.example, param.examples) : void 0
170
249
  };
171
250
  if (!parametersByName.has(param.name)) {
172
251
  parametersByName.set(param.name, []);
@@ -177,7 +256,8 @@ var ParameterResolver = class {
177
256
  const contentType = this.selectContentType(requestBody.content);
178
257
  const mediaType = requestBody.content[contentType];
179
258
  if (mediaType?.schema) {
180
- this.extractBodyParameters(mediaType.schema, parametersByName, requestBody.required ?? false, contentType);
259
+ const mediaExamples = this.includeExamples ? collectExampleValues(mediaType.example, mediaType.examples) : void 0;
260
+ this.extractBodyParameters(mediaType.schema, parametersByName, requestBody.required ?? false, contentType, mediaExamples, mediaType.encoding);
181
261
  }
182
262
  }
183
263
  const properties = {};
@@ -198,7 +278,9 @@ var ParameterResolver = class {
198
278
  required: param.required,
199
279
  style: param.style,
200
280
  explode: param.explode,
201
- serialization: param.serialization
281
+ allowReserved: param.allowReserved,
282
+ serialization: param.serialization,
283
+ ...param.wholeBody && { wholeBody: true }
202
284
  });
203
285
  } else {
204
286
  params.forEach((param, index) => {
@@ -214,7 +296,9 @@ var ParameterResolver = class {
214
296
  required: param.required,
215
297
  style: param.style,
216
298
  explode: param.explode,
217
- serialization: param.serialization
299
+ allowReserved: param.allowReserved,
300
+ serialization: param.serialization,
301
+ ...param.wholeBody && { wholeBody: true }
218
302
  });
219
303
  });
220
304
  }
@@ -239,23 +323,31 @@ var ParameterResolver = class {
239
323
  /**
240
324
  * Extract parameters from request body schema
241
325
  */
242
- extractBodyParameters(schema, parametersByName, required, contentType, prefix = "") {
326
+ extractBodyParameters(schema, parametersByName, required, contentType, mediaExamples, encoding, prefix = "") {
243
327
  if (!schema || typeof schema !== "object") return;
244
328
  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)) {
329
+ const flattened = flattenObjectBody(jsonSchema);
330
+ if (flattened) {
331
+ const requiredFields = flattened.required;
332
+ for (const [propName, propSchema] of Object.entries(flattened.properties)) {
248
333
  const fullName = prefix ? `${prefix}.${propName}` : propName;
249
334
  const isRequired = required && requiredFields.has(propName);
250
335
  if (typeof propSchema === "object") {
336
+ const propEncoding = encoding?.[propName];
337
+ const propExamples = mediaExamples?.map(
338
+ (ex) => ex !== null && typeof ex === "object" && !Array.isArray(ex) ? ex[propName] : void 0
339
+ ).filter((value) => value !== void 0);
251
340
  const info = {
252
341
  name: fullName,
253
342
  location: "body",
254
343
  required: isRequired,
255
344
  schema: propSchema,
256
345
  description: propSchema.description,
346
+ examples: propExamples && propExamples.length > 0 ? propExamples : void 0,
257
347
  serialization: {
258
- contentType
348
+ contentType,
349
+ ...propEncoding && { encoding: { [propName]: propEncoding } },
350
+ ...isBinarySchema(propSchema) && { binary: true }
259
351
  }
260
352
  };
261
353
  if (!parametersByName.has(fullName)) {
@@ -270,9 +362,13 @@ var ParameterResolver = class {
270
362
  name: bodyParamName,
271
363
  location: "body",
272
364
  required,
273
- schema,
365
+ schema: jsonSchema,
366
+ examples: mediaExamples,
367
+ wholeBody: true,
274
368
  serialization: {
275
- contentType
369
+ contentType,
370
+ ...encoding && Object.keys(encoding).length > 0 && { encoding },
371
+ ...isBinarySchema(jsonSchema) && { binary: true }
276
372
  }
277
373
  };
278
374
  if (!parametersByName.has(bodyParamName)) {
@@ -289,6 +385,9 @@ var ParameterResolver = class {
289
385
  if (param.description) {
290
386
  schema.description = param.description;
291
387
  }
388
+ if (param.examples && param.examples.length > 0) {
389
+ schema.examples = param.examples;
390
+ }
292
391
  if (param.deprecated) {
293
392
  schema["deprecated"] = true;
294
393
  }
@@ -385,21 +484,68 @@ var ParameterResolver = class {
385
484
  required: true,
386
485
  security: securityInfo
387
486
  });
388
- if (includeInInput) {
487
+ const schemeInInput = includeInInput === true || Array.isArray(includeInInput) && includeInInput.includes(scheme);
488
+ if (schemeInInput) {
389
489
  properties[inputKey] = schema;
390
490
  required.push(inputKey);
391
491
  }
392
492
  }
393
493
  }
394
494
  };
495
+ function collectObjectMembers(schema) {
496
+ if (!schema || typeof schema !== "object") return { properties: {}, required: /* @__PURE__ */ new Set() };
497
+ if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) return "union";
498
+ const properties = {};
499
+ const required = /* @__PURE__ */ new Set();
500
+ if (Array.isArray(schema.allOf)) {
501
+ for (const member of schema.allOf) {
502
+ const collected = collectObjectMembers(member);
503
+ if (collected === "union") return "union";
504
+ Object.assign(properties, collected.properties);
505
+ collected.required.forEach((field) => required.add(field));
506
+ }
507
+ }
508
+ if (schema.properties && typeof schema.properties === "object") {
509
+ Object.assign(properties, schema.properties);
510
+ }
511
+ if (Array.isArray(schema.required)) {
512
+ schema.required.forEach((field) => required.add(field));
513
+ }
514
+ return { properties, required };
515
+ }
516
+ function flattenObjectBody(schema) {
517
+ const collected = collectObjectMembers(schema);
518
+ if (collected === "union") return void 0;
519
+ return Object.keys(collected.properties).length > 0 ? collected : void 0;
520
+ }
521
+ function isBinarySchema(schema) {
522
+ if (!schema || typeof schema !== "object") return false;
523
+ const record = schema;
524
+ if (record["format"] === "binary") return true;
525
+ return typeof record["contentMediaType"] === "string" && record["contentEncoding"] === void 0 && record["type"] === void 0;
526
+ }
527
+ function collectExampleValues(example, examples) {
528
+ if (examples && !Array.isArray(examples)) {
529
+ const values = Object.values(examples).filter((entry) => entry !== null && typeof entry === "object" && !isReferenceObject(entry)).map((entry) => entry.value).filter((value) => value !== void 0);
530
+ if (values.length > 0) {
531
+ return values;
532
+ }
533
+ }
534
+ if (example !== void 0) {
535
+ return [example];
536
+ }
537
+ return void 0;
538
+ }
395
539
 
396
540
  // src/response-builder.ts
397
541
  var ResponseBuilder = class {
398
542
  preferredStatusCodes;
399
543
  includeAllResponses;
544
+ includeExamples;
400
545
  constructor(options = {}) {
401
546
  this.preferredStatusCodes = options.preferredStatusCodes ?? [200, 201, 204, 202, 203, 206];
402
547
  this.includeAllResponses = options.includeAllResponses ?? true;
548
+ this.includeExamples = options.includeExamples ?? false;
403
549
  }
404
550
  /**
405
551
  * Build output schema from responses
@@ -477,6 +623,12 @@ var ResponseBuilder = class {
477
623
  if (!schema.description && response.description) {
478
624
  schema.description = response.description;
479
625
  }
626
+ if (this.includeExamples) {
627
+ const mediaExamples = collectExampleValues(mediaType.example, mediaType.examples);
628
+ if (mediaExamples) {
629
+ schema.examples = mediaExamples;
630
+ }
631
+ }
480
632
  schema["x-content-type"] = contentType;
481
633
  return { statusCode, schema };
482
634
  }
@@ -513,239 +665,1620 @@ var ResponseBuilder = class {
513
665
  }
514
666
  };
515
667
 
516
- // src/validator.ts
517
- var Validator = class {
668
+ // src/schema-builder.ts
669
+ var SchemaBuilder = class {
518
670
  /**
519
- * Validate an OpenAPI document
671
+ * Merge multiple schemas into one
520
672
  */
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
- });
673
+ static merge(schemas) {
674
+ if (schemas.length === 0) {
675
+ return { type: "object" };
536
676
  }
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
- });
677
+ if (schemas.length === 1) {
678
+ return schemas[0];
679
+ }
680
+ const merged = {
681
+ type: "object",
682
+ properties: {},
683
+ required: []
684
+ };
685
+ const allRequired = /* @__PURE__ */ new Set();
686
+ for (const schema of schemas) {
687
+ if (schema.properties) {
688
+ merged.properties = {
689
+ ...merged.properties,
690
+ ...schema.properties
691
+ };
550
692
  }
551
- if (!document.info.version) {
552
- errors.push({
553
- message: "Missing required field: info.version",
554
- path: "/info/version",
555
- code: "MISSING_VERSION"
556
- });
693
+ if (schema.required) {
694
+ schema.required.forEach((field) => allRequired.add(field));
557
695
  }
558
696
  }
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);
697
+ if (allRequired.size > 0) {
698
+ merged.required = Array.from(allRequired);
567
699
  }
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
- });
700
+ return merged;
701
+ }
702
+ /**
703
+ * Create a union schema (oneOf)
704
+ */
705
+ static union(schemas) {
706
+ if (schemas.length === 0) {
707
+ return {};
574
708
  }
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
- });
709
+ if (schemas.length === 1) {
710
+ return schemas[0];
581
711
  }
582
712
  return {
583
- valid: errors.length === 0,
584
- errors: errors.length > 0 ? errors : void 0,
585
- warnings: warnings.length > 0 ? warnings : void 0
713
+ oneOf: schemas
586
714
  };
587
715
  }
588
716
  /**
589
- * Check if OpenAPI version is valid
717
+ * Deep clone a schema
590
718
  */
591
- isValidOpenAPIVersion(version) {
592
- return /^3\.[01]\.\d+$/.test(version);
719
+ static clone(schema) {
720
+ return JSON.parse(JSON.stringify(schema));
593
721
  }
594
722
  /**
595
- * Validate paths
723
+ * Remove $ref from schema (assumes already dereferenced)
596
724
  */
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);
725
+ static removeRefs(schema) {
726
+ const cloned = this.clone(schema);
727
+ this.removeRefsRecursive(cloned);
728
+ return cloned;
729
+ }
730
+ static removeRefsRecursive(obj) {
731
+ if (!obj || typeof obj !== "object") return;
732
+ if (obj.$ref) {
733
+ delete obj.$ref;
734
+ }
735
+ for (const key in obj) {
736
+ if (key in obj) {
737
+ const value = obj[key];
738
+ if (value && typeof value === "object") {
739
+ this.removeRefsRecursive(value);
614
740
  }
615
741
  }
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
742
  }
624
743
  }
625
744
  /**
626
- * Validate an operation
745
+ * Add description to schema
627
746
  */
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
- }
747
+ static withDescription(schema, description) {
748
+ return {
749
+ ...schema,
750
+ description
751
+ };
660
752
  }
661
753
  /**
662
- * Validate parameters
754
+ * Add example to schema
663
755
  */
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
- }
756
+ static withExample(schema, example) {
757
+ const existingExamples = Array.isArray(schema.examples) ? schema.examples : [];
758
+ return {
759
+ ...schema,
760
+ examples: [...existingExamples, example]
761
+ };
762
+ }
763
+ /**
764
+ * Add default value to schema
765
+ */
766
+ static withDefault(schema, defaultValue) {
767
+ return {
768
+ ...schema,
769
+ default: defaultValue
770
+ };
771
+ }
772
+ /**
773
+ * Add format to schema
774
+ */
775
+ static withFormat(schema, format) {
776
+ return {
777
+ ...schema,
778
+ format
779
+ };
780
+ }
781
+ /**
782
+ * Add pattern to schema
783
+ */
784
+ static withPattern(schema, pattern) {
785
+ return {
786
+ ...schema,
787
+ pattern
788
+ };
789
+ }
790
+ /**
791
+ * Add enum to schema
792
+ */
793
+ static withEnum(schema, values) {
794
+ return {
795
+ ...schema,
796
+ enum: values
797
+ };
798
+ }
799
+ /**
800
+ * Add minimum/maximum constraints
801
+ */
802
+ static withRange(schema, min, max, options = {}) {
803
+ const result = { ...schema };
804
+ if (min !== void 0) {
805
+ if (options.exclusive) {
806
+ result.exclusiveMinimum = min;
807
+ } else {
808
+ result.minimum = min;
809
+ }
810
+ }
811
+ if (max !== void 0) {
812
+ if (options.exclusive) {
813
+ result.exclusiveMaximum = max;
814
+ } else {
815
+ result.maximum = max;
816
+ }
817
+ }
818
+ return result;
819
+ }
820
+ /**
821
+ * Add minLength/maxLength constraints
822
+ */
823
+ static withLength(schema, minLength, maxLength) {
824
+ const result = { ...schema };
825
+ if (minLength !== void 0) {
826
+ result.minLength = minLength;
827
+ }
828
+ if (maxLength !== void 0) {
829
+ result.maxLength = maxLength;
830
+ }
831
+ return result;
832
+ }
833
+ /**
834
+ * Create object schema
835
+ */
836
+ static object(properties, required) {
837
+ return {
838
+ type: "object",
839
+ properties,
840
+ ...required && required.length > 0 && { required },
841
+ additionalProperties: false
842
+ };
843
+ }
844
+ /**
845
+ * Create array schema
846
+ */
847
+ static array(items, constraints) {
848
+ return {
849
+ type: "array",
850
+ items,
851
+ ...constraints
852
+ };
853
+ }
854
+ /**
855
+ * Create string schema
856
+ */
857
+ static string(constraints) {
858
+ return {
859
+ type: "string",
860
+ ...constraints
861
+ };
862
+ }
863
+ /**
864
+ * Create number schema
865
+ */
866
+ static number(constraints) {
867
+ return {
868
+ type: "number",
869
+ ...constraints
870
+ };
871
+ }
872
+ /**
873
+ * Create integer schema
874
+ */
875
+ static integer(constraints) {
876
+ return {
877
+ type: "integer",
878
+ ...constraints
879
+ };
880
+ }
881
+ /**
882
+ * Create boolean schema
883
+ */
884
+ static boolean() {
885
+ return {
886
+ type: "boolean"
887
+ };
888
+ }
889
+ /**
890
+ * Create null schema
891
+ */
892
+ static null() {
893
+ return {
894
+ type: "null"
895
+ };
896
+ }
897
+ /**
898
+ * Flatten nested oneOf/anyOf/allOf schemas
899
+ */
900
+ static flatten(schema, maxDepth = 10) {
901
+ if (maxDepth <= 0) return schema;
902
+ const cloned = this.clone(schema);
903
+ if (cloned.oneOf) {
904
+ const flattened = cloned.oneOf.flatMap((s) => {
905
+ const sub = this.flatten(s, maxDepth - 1);
906
+ return sub.oneOf ? sub.oneOf : [sub];
907
+ });
908
+ cloned.oneOf = flattened;
909
+ }
910
+ if (cloned.anyOf) {
911
+ const flattened = cloned.anyOf.flatMap((s) => {
912
+ const sub = this.flatten(s, maxDepth - 1);
913
+ return sub.anyOf ? sub.anyOf : [sub];
914
+ });
915
+ cloned.anyOf = flattened;
916
+ }
917
+ if (cloned.allOf) {
918
+ const flattened = cloned.allOf.flatMap((s) => {
919
+ const sub = this.flatten(s, maxDepth - 1);
920
+ return sub.allOf ? sub.allOf : [sub];
921
+ });
922
+ cloned.allOf = flattened;
923
+ }
924
+ return cloned;
925
+ }
926
+ /**
927
+ * Truncate a schema tree to a maximum nesting depth.
928
+ *
929
+ * The root sits at depth 0; descending into `properties` values, `items`,
930
+ * `additionalProperties`, composition members (`allOf`/`anyOf`/`oneOf`), or
931
+ * `not` increments the depth. Nodes at `maxDepth` keep their scalar keywords
932
+ * (type, description, format, ...) but have their child schemas stripped and
933
+ * a truncation note appended to the description.
934
+ */
935
+ static truncateDepth(schema, maxDepth) {
936
+ const bound = Number.isFinite(maxDepth) ? Math.max(0, Math.floor(maxDepth)) : 10;
937
+ return this.truncateDepthRecursive(schema, 0, bound);
938
+ }
939
+ /** Keys whose value is a map of schemas (JSON Schema 2020-12) */
940
+ static TRUNCATE_MAP_KEYS = [
941
+ "properties",
942
+ "patternProperties",
943
+ "$defs",
944
+ "definitions",
945
+ "dependentSchemas"
946
+ ];
947
+ /** Keys whose value is a single schema (or, for `items`, a tuple array) */
948
+ static TRUNCATE_SCHEMA_KEYS = [
949
+ "items",
950
+ "additionalProperties",
951
+ "not",
952
+ "if",
953
+ "then",
954
+ "else",
955
+ "propertyNames",
956
+ "contains",
957
+ "contentSchema",
958
+ "unevaluatedProperties",
959
+ "unevaluatedItems"
960
+ ];
961
+ /** Keys whose value is an array of schemas */
962
+ static TRUNCATE_LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
963
+ // Copy-on-walk: never mutates the input, only copies nodes that have schema
964
+ // children, and — because the walk is depth-bounded — terminates even on
965
+ // circular schema graphs (which `clone()`'s JSON round-trip would reject).
966
+ static truncateDepthRecursive(node, depth, maxDepth) {
967
+ if (!node || typeof node !== "object") return node;
968
+ const record = node;
969
+ const childKeys = [...this.TRUNCATE_MAP_KEYS, ...this.TRUNCATE_SCHEMA_KEYS, ...this.TRUNCATE_LIST_KEYS];
970
+ const hasChildren = childKeys.some((key) => {
971
+ const value = record[key];
972
+ return value !== null && typeof value === "object";
973
+ });
974
+ if (!hasChildren) return node;
975
+ const copy = { ...node };
976
+ const copyRecord = copy;
977
+ if (depth >= maxDepth) {
978
+ for (const key of childKeys) {
979
+ const value = copyRecord[key];
980
+ if (value !== null && typeof value === "object") {
981
+ delete copyRecord[key];
982
+ }
983
+ }
984
+ delete copyRecord["required"];
985
+ const note = "[Truncated: nested schema exceeds maxSchemaDepth]";
986
+ copy.description = copy.description ? `${copy.description} ${note}` : note;
987
+ return copy;
988
+ }
989
+ for (const key of this.TRUNCATE_MAP_KEYS) {
990
+ const value = copyRecord[key];
991
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
992
+ const mapped = {};
993
+ for (const [name, sub] of Object.entries(value)) {
994
+ mapped[name] = this.truncateDepthRecursive(sub, depth + 1, maxDepth);
995
+ }
996
+ copyRecord[key] = mapped;
997
+ }
998
+ }
999
+ for (const key of this.TRUNCATE_SCHEMA_KEYS) {
1000
+ const value = copyRecord[key];
1001
+ if (value !== null && typeof value === "object") {
1002
+ copyRecord[key] = Array.isArray(value) ? value.map((item) => this.truncateDepthRecursive(item, depth + 1, maxDepth)) : this.truncateDepthRecursive(value, depth + 1, maxDepth);
1003
+ }
1004
+ }
1005
+ for (const key of this.TRUNCATE_LIST_KEYS) {
1006
+ const value = copyRecord[key];
1007
+ if (Array.isArray(value)) {
1008
+ copyRecord[key] = value.map((member) => this.truncateDepthRecursive(member, depth + 1, maxDepth));
1009
+ }
1010
+ }
1011
+ return copy;
1012
+ }
1013
+ // Copy-on-walk over every structural keyword (same key groups as
1014
+ // truncateDepth): `visit` transforms each node top-down and must return a
1015
+ // new node when it changes anything.
1016
+ static walkCopy(node, visit, seen = /* @__PURE__ */ new Map()) {
1017
+ if (!node || typeof node !== "object") return node;
1018
+ const existing = seen.get(node);
1019
+ if (existing) return existing;
1020
+ const copy = visit({ ...node });
1021
+ seen.set(node, copy);
1022
+ for (const key of this.TRUNCATE_MAP_KEYS) {
1023
+ const value = copy[key];
1024
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1025
+ const mapped = {};
1026
+ for (const [name, sub] of Object.entries(value)) {
1027
+ mapped[name] = this.walkCopy(sub, visit, seen);
1028
+ }
1029
+ copy[key] = mapped;
1030
+ }
1031
+ }
1032
+ for (const key of this.TRUNCATE_SCHEMA_KEYS) {
1033
+ const value = copy[key];
1034
+ if (Array.isArray(value)) {
1035
+ copy[key] = value.map((item) => this.walkCopy(item, visit, seen));
1036
+ } else if (value !== null && typeof value === "object") {
1037
+ copy[key] = this.walkCopy(value, visit, seen);
1038
+ }
1039
+ }
1040
+ for (const key of this.TRUNCATE_LIST_KEYS) {
1041
+ const value = copy[key];
1042
+ if (Array.isArray(value)) {
1043
+ copy[key] = value.map((member) => this.walkCopy(member, visit, seen));
1044
+ }
1045
+ }
1046
+ return copy;
1047
+ }
1048
+ /**
1049
+ * Limit every object node to its first `max` properties (declaration
1050
+ * order). Dropped properties are pruned from `required` and counted in a
1051
+ * note appended to the node's description.
1052
+ */
1053
+ static limitProperties(schema, max) {
1054
+ const bound = Number.isFinite(max) ? Math.max(1, Math.floor(max)) : Number.MAX_SAFE_INTEGER;
1055
+ return this.walkCopy(schema, (node) => {
1056
+ const properties = node.properties;
1057
+ if (!properties || typeof properties !== "object") return node;
1058
+ const entries = Object.entries(properties);
1059
+ if (entries.length <= bound) return node;
1060
+ const kept = entries.slice(0, bound);
1061
+ const keptNames = new Set(kept.map(([name]) => name));
1062
+ const dropped = entries.length - bound;
1063
+ const note = `[${dropped} additional propert${dropped === 1 ? "y" : "ies"} omitted: exceeds maxProperties]`;
1064
+ const next = { ...node, properties: Object.fromEntries(kept) };
1065
+ if (Array.isArray(node.required)) {
1066
+ const required = node.required.filter((name) => keptNames.has(String(name)));
1067
+ if (required.length > 0) {
1068
+ next.required = required;
1069
+ } else {
1070
+ delete next.required;
1071
+ }
1072
+ }
1073
+ next.description = node.description ? `${node.description} ${note}` : note;
1074
+ return next;
1075
+ });
1076
+ }
1077
+ /**
1078
+ * Cap every description in the schema tree to `maxLength` characters,
1079
+ * truncating with an ellipsis.
1080
+ */
1081
+ static capDescriptions(schema, maxLength) {
1082
+ const bound = Number.isFinite(maxLength) ? Math.max(1, Math.floor(maxLength)) : Number.MAX_SAFE_INTEGER;
1083
+ return this.walkCopy(schema, (node) => {
1084
+ if (typeof node.description === "string" && node.description.length > bound) {
1085
+ return { ...node, description: `${node.description.slice(0, bound - 1)}\u2026` };
1086
+ }
1087
+ return node;
1088
+ });
1089
+ }
1090
+ /**
1091
+ * Remove every `examples` array from the schema tree (a token-budget
1092
+ * trimming step — validation keywords are untouched).
1093
+ */
1094
+ static stripExamples(schema) {
1095
+ return this.walkCopy(schema, (node) => {
1096
+ if ("examples" in node) {
1097
+ const { examples: _examples, ...rest } = node;
1098
+ return rest;
1099
+ }
1100
+ return node;
1101
+ });
1102
+ }
1103
+ /**
1104
+ * Simplify schema by removing unnecessary fields
1105
+ */
1106
+ static simplify(schema) {
1107
+ const cloned = this.clone(schema);
1108
+ if (Array.isArray(cloned.required) && cloned.required.length === 0) {
1109
+ delete cloned.required;
1110
+ }
1111
+ if (cloned.properties && Object.keys(cloned.properties).length === 0) {
1112
+ delete cloned.properties;
1113
+ }
1114
+ if (Array.isArray(cloned.examples) && cloned.examples.length === 0) {
1115
+ delete cloned.examples;
1116
+ }
1117
+ if (cloned.title && cloned.description && cloned.title === cloned.description) {
1118
+ delete cloned.title;
1119
+ }
1120
+ return cloned;
1121
+ }
1122
+ };
1123
+
1124
+ // src/annotations.ts
1125
+ function inferAnnotationsFromMethod(method) {
1126
+ switch (method) {
1127
+ case "get":
1128
+ case "head":
1129
+ case "options":
1130
+ case "trace":
1131
+ return { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
1132
+ case "put":
1133
+ case "delete":
1134
+ return { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false };
1135
+ case "post":
1136
+ case "patch":
1137
+ return { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
1138
+ }
1139
+ }
1140
+ var ANNOTATION_KEYS = ["title", "readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"];
1141
+ function pickAnnotations(raw) {
1142
+ if (!raw || typeof raw !== "object") return void 0;
1143
+ const result = {};
1144
+ for (const key of ANNOTATION_KEYS) {
1145
+ const value = raw[key];
1146
+ if (key === "title" ? typeof value === "string" : typeof value === "boolean") {
1147
+ result[key] = value;
1148
+ }
1149
+ }
1150
+ return Object.keys(result).length > 0 ? result : void 0;
1151
+ }
1152
+ function mergeOverrides(base, layer) {
1153
+ return {
1154
+ ...base,
1155
+ ...layer.disabled !== void 0 && { disabled: layer.disabled },
1156
+ ...layer.name !== void 0 && { name: layer.name },
1157
+ ...layer.title !== void 0 && { title: layer.title },
1158
+ ...layer.description !== void 0 && { description: layer.description },
1159
+ ...(base.annotations || layer.annotations) && {
1160
+ annotations: { ...base.annotations, ...layer.annotations }
1161
+ }
1162
+ };
1163
+ }
1164
+ function readXMcp(node) {
1165
+ return node["x-mcp"];
1166
+ }
1167
+ function parseXMcpEnabled(ext) {
1168
+ if (ext === false) return false;
1169
+ if (ext === true) return true;
1170
+ if (ext && typeof ext === "object" && typeof ext.enabled === "boolean") {
1171
+ return ext.enabled;
1172
+ }
1173
+ return void 0;
1174
+ }
1175
+ function resolveExtensionEnabled(document, pathItem, operation) {
1176
+ let enabled = true;
1177
+ const rootSetting = parseXMcpEnabled(readXMcp(document));
1178
+ if (rootSetting !== void 0) enabled = rootSetting;
1179
+ const pathSetting = parseXMcpEnabled(readXMcp(pathItem));
1180
+ if (pathSetting !== void 0) enabled = pathSetting;
1181
+ const operationDisabled = extractExtensionOverrides(operation).disabled;
1182
+ if (operationDisabled !== void 0) enabled = !operationDisabled;
1183
+ return enabled;
1184
+ }
1185
+ function extractExtensionOverrides(operation) {
1186
+ const op = operation;
1187
+ let result = {};
1188
+ const speakeasy = op["x-speakeasy-mcp"];
1189
+ if (speakeasy && typeof speakeasy === "object") {
1190
+ const ext = speakeasy;
1191
+ result = mergeOverrides(result, {
1192
+ disabled: typeof ext["disabled"] === "boolean" ? ext["disabled"] : void 0,
1193
+ name: typeof ext["name"] === "string" ? ext["name"] : void 0,
1194
+ title: typeof ext["title"] === "string" ? ext["title"] : void 0,
1195
+ description: typeof ext["description"] === "string" ? ext["description"] : void 0,
1196
+ // Speakeasy's top-level `title` is the tool title, not an annotation slot
1197
+ annotations: pickAnnotations({ ...ext, title: void 0 })
1198
+ });
1199
+ }
1200
+ const xMcp = op["x-mcp"];
1201
+ if (xMcp === false) {
1202
+ result = mergeOverrides(result, { disabled: true });
1203
+ } else if (xMcp === true) {
1204
+ result = mergeOverrides(result, { disabled: false });
1205
+ } else if (xMcp && typeof xMcp === "object") {
1206
+ const ext = xMcp;
1207
+ result = mergeOverrides(result, {
1208
+ disabled: typeof ext["enabled"] === "boolean" ? !ext["enabled"] : void 0,
1209
+ name: typeof ext["name"] === "string" ? ext["name"] : void 0,
1210
+ title: typeof ext["title"] === "string" ? ext["title"] : void 0,
1211
+ description: typeof ext["description"] === "string" ? ext["description"] : void 0,
1212
+ annotations: pickAnnotations(ext["annotations"])
1213
+ });
1214
+ }
1215
+ const frontmcp = op["x-frontmcp"];
1216
+ if (frontmcp && typeof frontmcp === "object" && frontmcp.annotations) {
1217
+ const annotations = pickAnnotations(frontmcp.annotations);
1218
+ result = mergeOverrides(result, {
1219
+ annotations,
1220
+ title: typeof frontmcp.annotations.title === "string" ? frontmcp.annotations.title : void 0
1221
+ });
1222
+ }
1223
+ return result;
1224
+ }
1225
+
1226
+ // src/client-targets.ts
1227
+ var MAP_KEYS = ["properties", "patternProperties", "dependentSchemas"];
1228
+ var SCHEMA_KEYS = [
1229
+ "items",
1230
+ "additionalProperties",
1231
+ "not",
1232
+ "if",
1233
+ "then",
1234
+ "else",
1235
+ "propertyNames",
1236
+ "contains",
1237
+ "contentSchema",
1238
+ "unevaluatedItems",
1239
+ "unevaluatedProperties"
1240
+ ];
1241
+ var LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
1242
+ function isSchemaObject(value) {
1243
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1244
+ }
1245
+ function walkSchema(node, visit) {
1246
+ if (!isSchemaObject(node)) return node;
1247
+ const visited = visit({ ...node });
1248
+ for (const key of MAP_KEYS) {
1249
+ const value = visited[key];
1250
+ if (isSchemaObject(value)) {
1251
+ const mapped = {};
1252
+ for (const [name, sub] of Object.entries(value)) {
1253
+ mapped[name] = walkSchema(sub, visit);
1254
+ }
1255
+ visited[key] = mapped;
1256
+ }
1257
+ }
1258
+ for (const key of SCHEMA_KEYS) {
1259
+ const value = visited[key];
1260
+ if (Array.isArray(value)) {
1261
+ visited[key] = value.map((item) => walkSchema(item, visit));
1262
+ } else if (isSchemaObject(value)) {
1263
+ visited[key] = walkSchema(value, visit);
1264
+ }
1265
+ }
1266
+ for (const key of LIST_KEYS) {
1267
+ const value = visited[key];
1268
+ if (Array.isArray(value)) {
1269
+ visited[key] = value.map((member) => walkSchema(member, visit));
1270
+ }
1271
+ }
1272
+ return visited;
1273
+ }
1274
+ function inlineLocalRefs(schema) {
1275
+ if (!isSchemaObject(schema)) return schema;
1276
+ const root = schema;
1277
+ const resolvePointer = (pointer) => {
1278
+ const parts = pointer.replace(/^#\/?/, "").split("/").filter((part) => part.length > 0).map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"));
1279
+ let current = root;
1280
+ for (const part of parts) {
1281
+ if (!isSchemaObject(current)) return void 0;
1282
+ current = current[part];
1283
+ }
1284
+ return current;
1285
+ };
1286
+ const inline = (node, seenPointers) => {
1287
+ if (!isSchemaObject(node)) return node;
1288
+ const record = node;
1289
+ const ref = record["$ref"];
1290
+ if (typeof ref === "string" && !ref.startsWith("#")) {
1291
+ const { $ref: _external, ...siblings } = record;
1292
+ return inline(
1293
+ { description: `[External $ref ${ref} removed for client compatibility]`, ...siblings },
1294
+ seenPointers
1295
+ );
1296
+ }
1297
+ if (typeof ref === "string") {
1298
+ const { $ref: _ref, ...siblings } = record;
1299
+ if (seenPointers.has(ref)) {
1300
+ return { description: "[Circular $ref removed for client compatibility]", ...siblings };
1301
+ }
1302
+ const resolved = resolvePointer(ref);
1303
+ if (!isSchemaObject(resolved)) {
1304
+ return { description: `[Unresolvable $ref ${ref} removed for client compatibility]`, ...siblings };
1305
+ }
1306
+ const inlined = inline(resolved, /* @__PURE__ */ new Set([...seenPointers, ref]));
1307
+ if (!isSchemaObject(inlined)) return inlined;
1308
+ return { ...inlined, ...siblings };
1309
+ }
1310
+ const copy = { ...record };
1311
+ delete copy["$defs"];
1312
+ delete copy["definitions"];
1313
+ for (const key of MAP_KEYS) {
1314
+ const value = copy[key];
1315
+ if (isSchemaObject(value)) {
1316
+ const mapped = {};
1317
+ for (const [name, sub] of Object.entries(value)) {
1318
+ mapped[name] = inline(sub, seenPointers);
1319
+ }
1320
+ copy[key] = mapped;
1321
+ }
1322
+ }
1323
+ for (const key of SCHEMA_KEYS) {
1324
+ const value = copy[key];
1325
+ if (Array.isArray(value)) {
1326
+ copy[key] = value.map((item) => inline(item, seenPointers));
1327
+ } else if (isSchemaObject(value)) {
1328
+ copy[key] = inline(value, seenPointers);
1329
+ }
1330
+ }
1331
+ for (const key of LIST_KEYS) {
1332
+ const value = copy[key];
1333
+ if (Array.isArray(value)) {
1334
+ copy[key] = value.map((member) => inline(member, seenPointers));
1335
+ }
1336
+ }
1337
+ return copy;
1338
+ };
1339
+ return inline(schema, /* @__PURE__ */ new Set());
1340
+ }
1341
+ function ensureArrayItems(schema) {
1342
+ return walkSchema(schema, (node) => {
1343
+ const type = node["type"];
1344
+ const isArray = type === "array" || Array.isArray(type) && type.includes("array");
1345
+ if (isArray && node["items"] === void 0) {
1346
+ return { ...node, items: {} };
1347
+ }
1348
+ return node;
1349
+ });
1350
+ }
1351
+ function mergeAllOf(node) {
1352
+ const members = node["allOf"];
1353
+ const merged = {};
1354
+ const properties = {};
1355
+ const required = /* @__PURE__ */ new Set();
1356
+ for (const rawMember of members) {
1357
+ if (!isSchemaObject(rawMember)) continue;
1358
+ const member = Array.isArray(rawMember["allOf"]) ? mergeAllOf(rawMember) : rawMember;
1359
+ const { properties: memberProps, required: memberRequired, ...scalars } = member;
1360
+ Object.assign(merged, scalars);
1361
+ if (isSchemaObject(memberProps)) Object.assign(properties, memberProps);
1362
+ if (Array.isArray(memberRequired)) memberRequired.forEach((field) => required.add(String(field)));
1363
+ }
1364
+ const { allOf: _allOf, properties: ownProps, required: ownRequired, ...rest } = node;
1365
+ Object.assign(merged, rest);
1366
+ if (isSchemaObject(ownProps)) Object.assign(properties, ownProps);
1367
+ if (Array.isArray(ownRequired)) ownRequired.forEach((field) => required.add(String(field)));
1368
+ if (Object.keys(properties).length > 0) merged["properties"] = properties;
1369
+ if (required.size > 0) merged["required"] = [...required];
1370
+ return merged;
1371
+ }
1372
+ function nullableWrapperMember(node) {
1373
+ const anyOf = node["anyOf"];
1374
+ if (!Array.isArray(anyOf) || anyOf.length !== 2) return void 0;
1375
+ const nullIndex = anyOf.findIndex((m) => isSchemaObject(m) && m["type"] === "null");
1376
+ if (nullIndex === -1) return void 0;
1377
+ const other = anyOf[1 - nullIndex];
1378
+ return isSchemaObject(other) ? other : void 0;
1379
+ }
1380
+ function describeVariants(members) {
1381
+ return members.map((member, index) => {
1382
+ if (!isSchemaObject(member)) return `variant ${index + 1}`;
1383
+ const record = member;
1384
+ return typeof record["title"] === "string" && record["title"] || typeof record["description"] === "string" && record["description"] || typeof record["type"] === "string" && `type ${record["type"]}` || `variant ${index + 1}`;
1385
+ }).join("; ");
1386
+ }
1387
+ function collapseRootCompositions(schema) {
1388
+ if (!isSchemaObject(schema)) return schema;
1389
+ const node = { ...schema };
1390
+ if (Array.isArray(node["allOf"])) {
1391
+ return collapseRootCompositions(mergeAllOf(node));
1392
+ }
1393
+ const nullableMember = nullableWrapperMember(node);
1394
+ if (nullableMember) {
1395
+ const { anyOf: _anyOf, ...rest } = node;
1396
+ const merged = { ...nullableMember, ...rest };
1397
+ const note = "May be null.";
1398
+ merged["description"] = merged["description"] ? `${merged["description"]} ${note}` : note;
1399
+ return merged;
1400
+ }
1401
+ for (const key of ["oneOf", "anyOf"]) {
1402
+ const members = node[key];
1403
+ if (Array.isArray(members)) {
1404
+ const { [key]: _members, ...rest } = node;
1405
+ return {
1406
+ ...rest,
1407
+ description: `${typeof rest["description"] === "string" ? `${rest["description"]} ` : ""}Accepts one of ${members.length} variants: ${describeVariants(members)}.`,
1408
+ "x-variants": members
1409
+ };
1410
+ }
1411
+ }
1412
+ return node;
1413
+ }
1414
+ function collapseNestedUnions(schema) {
1415
+ return walkSchema(schema, (node) => {
1416
+ let current = node;
1417
+ for (; ; ) {
1418
+ if (Array.isArray(current["allOf"])) {
1419
+ current = mergeAllOf(current);
1420
+ continue;
1421
+ }
1422
+ const type = current["type"];
1423
+ if (Array.isArray(type)) {
1424
+ const nonNull = type.filter((t) => t !== "null");
1425
+ const notes = [];
1426
+ if (nonNull.length > 1) notes.push(`Alternative types accepted: ${nonNull.slice(1).join(", ")}.`);
1427
+ if (nonNull.length !== type.length) notes.push("May be null.");
1428
+ current = { ...current, type: nonNull[0] ?? "null" };
1429
+ if (notes.length > 0) {
1430
+ const joined = notes.join(" ");
1431
+ current["description"] = current["description"] ? `${current["description"]} ${joined}` : joined;
1432
+ }
1433
+ continue;
1434
+ }
1435
+ const nullableMember = nullableWrapperMember(current);
1436
+ if (nullableMember) {
1437
+ const { anyOf: _anyOf, ...rest } = current;
1438
+ const merged = { ...nullableMember, ...rest };
1439
+ const note = "May be null.";
1440
+ merged["description"] = merged["description"] ? `${merged["description"]} ${note}` : note;
1441
+ current = merged;
1442
+ continue;
1443
+ }
1444
+ let collapsedUnion = false;
1445
+ for (const key of ["oneOf", "anyOf"]) {
1446
+ const members = current[key];
1447
+ if (Array.isArray(members) && members.length > 0 && isSchemaObject(members[0])) {
1448
+ const { [key]: _members, ...rest } = current;
1449
+ const first = { ...members[0] };
1450
+ const note = members.length > 1 ? `${members.length - 1} alternative schema variant(s) omitted for client compatibility: ${describeVariants(
1451
+ members.slice(1)
1452
+ )}.` : void 0;
1453
+ const merged = { ...first, ...rest };
1454
+ if (note) {
1455
+ merged["description"] = merged["description"] ? `${merged["description"]} ${note}` : note;
1456
+ }
1457
+ current = merged;
1458
+ collapsedUnion = true;
1459
+ break;
1460
+ }
1461
+ }
1462
+ if (collapsedUnion) continue;
1463
+ return current;
1464
+ }
1465
+ });
1466
+ }
1467
+ var GEMINI_SUPPORTED_FORMATS = /* @__PURE__ */ new Set(["date-time", "enum"]);
1468
+ var GEMINI_NUMERIC_FORMATS = /* @__PURE__ */ new Set(["int32", "int64", "float", "double"]);
1469
+ function isNumericNode(node) {
1470
+ const type = node["type"];
1471
+ return type === "integer" || type === "number" || Array.isArray(type) && (type.includes("integer") || type.includes("number"));
1472
+ }
1473
+ function demoteFormats(schema, supported = GEMINI_SUPPORTED_FORMATS) {
1474
+ return walkSchema(schema, (node) => {
1475
+ const format = node["format"];
1476
+ if (typeof format !== "string" || supported.has(format)) return node;
1477
+ if (GEMINI_NUMERIC_FORMATS.has(format) && isNumericNode(node)) return node;
1478
+ const { format: _format, ...rest } = node;
1479
+ const note = `(format: ${format})`;
1480
+ rest["description"] = rest["description"] ? `${rest["description"]} ${note}` : note;
1481
+ return rest;
1482
+ });
1483
+ }
1484
+ function isObjectNode(node) {
1485
+ const type = node["type"];
1486
+ return type === "object" || Array.isArray(type) && type.includes("object") || type === void 0 && isSchemaObject(node["properties"]);
1487
+ }
1488
+ function enforceClosedObjects(schema) {
1489
+ return walkSchema(schema, (node) => {
1490
+ if (isObjectNode(node) && (node["additionalProperties"] === void 0 || node["additionalProperties"] === true)) {
1491
+ return { ...node, additionalProperties: false };
1492
+ }
1493
+ return node;
1494
+ });
1495
+ }
1496
+ function requireAllProperties(schema) {
1497
+ return walkSchema(schema, (node) => {
1498
+ if (!isObjectNode(node) || !isSchemaObject(node["properties"])) return node;
1499
+ const properties = node["properties"];
1500
+ const originallyRequired = new Set(Array.isArray(node["required"]) ? node["required"].map(String) : []);
1501
+ const rewritten = {};
1502
+ for (const [name, propSchema] of Object.entries(properties)) {
1503
+ if (originallyRequired.has(name) || !isSchemaObject(propSchema) || propSchema["const"] !== void 0) {
1504
+ rewritten[name] = propSchema;
1505
+ continue;
1506
+ }
1507
+ const prop = propSchema;
1508
+ const withNullEnum = (next) => {
1509
+ const enumValues = next["enum"];
1510
+ if (Array.isArray(enumValues) && !enumValues.includes(null)) {
1511
+ return { ...next, enum: [...enumValues, null] };
1512
+ }
1513
+ return next;
1514
+ };
1515
+ const type = prop["type"];
1516
+ if (typeof type === "string" && type !== "null") {
1517
+ rewritten[name] = withNullEnum({ ...prop, type: [type, "null"] });
1518
+ } else if (Array.isArray(type) && !type.includes("null")) {
1519
+ rewritten[name] = withNullEnum({ ...prop, type: [...type, "null"] });
1520
+ } else {
1521
+ rewritten[name] = withNullEnum(prop);
1522
+ }
1523
+ }
1524
+ return { ...node, properties: rewritten, required: Object.keys(properties) };
1525
+ });
1526
+ }
1527
+ function applyClientTarget(schema, target) {
1528
+ let result = inlineLocalRefs(schema);
1529
+ result = ensureArrayItems(result);
1530
+ if (target === "gemini") {
1531
+ result = collapseNestedUnions(result);
1532
+ result = demoteFormats(result);
1533
+ return result;
1534
+ }
1535
+ result = collapseRootCompositions(result);
1536
+ if (target === "openai") {
1537
+ result = enforceClosedObjects(result);
1538
+ result = requireAllProperties(result);
1539
+ }
1540
+ return result;
1541
+ }
1542
+
1543
+ // src/errors.ts
1544
+ var OpenAPIToolError = class extends Error {
1545
+ context;
1546
+ constructor(message, context) {
1547
+ super(message);
1548
+ this.name = this.constructor.name;
1549
+ this.context = context;
1550
+ if (Error.captureStackTrace) {
1551
+ Error.captureStackTrace(this, this.constructor);
1552
+ }
1553
+ }
1554
+ };
1555
+ var LoadError = class extends OpenAPIToolError {
1556
+ constructor(message, context) {
1557
+ super(message, context);
1558
+ }
1559
+ };
1560
+ var SsrfError = class extends LoadError {
1561
+ constructor(message, context) {
1562
+ super(message, context);
1563
+ }
1564
+ };
1565
+ var ParseError = class extends OpenAPIToolError {
1566
+ constructor(message, context) {
1567
+ super(message, context);
1568
+ }
1569
+ };
1570
+ var ValidationError = class extends OpenAPIToolError {
1571
+ errors;
1572
+ constructor(message, context) {
1573
+ super(message, context);
1574
+ this.errors = context?.["errors"];
1575
+ }
1576
+ };
1577
+ var GenerationError = class extends OpenAPIToolError {
1578
+ constructor(message, context) {
1579
+ super(message, context);
1580
+ }
1581
+ };
1582
+ var OverlayError = class extends OpenAPIToolError {
1583
+ constructor(message, context) {
1584
+ super(message, context);
1585
+ }
1586
+ };
1587
+ var RequestBuildError = class extends OpenAPIToolError {
1588
+ constructor(message, context) {
1589
+ super(message, context);
1590
+ }
1591
+ };
1592
+ var SchemaError = class extends OpenAPIToolError {
1593
+ constructor(message, context) {
1594
+ super(message, context);
704
1595
  }
705
1596
  };
706
1597
 
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);
1598
+ // src/overlay.ts
1599
+ function parsePath(path) {
1600
+ if (typeof path !== "string" || !path.startsWith("$")) {
1601
+ throw new OverlayError(`Overlay target must be a JSONPath starting with '$'; received '${String(path)}'`, {
1602
+ target: path
1603
+ });
1604
+ }
1605
+ const segments = [];
1606
+ let rest = path.slice(1);
1607
+ while (rest.length > 0) {
1608
+ let recursive = false;
1609
+ if (rest.startsWith("..")) {
1610
+ recursive = true;
1611
+ rest = rest.slice(2);
1612
+ const bare = rest.match(/^([A-Za-z_][\w-]*)/);
1613
+ if (bare) {
1614
+ segments.push({ kind: "child", name: bare[1], recursive });
1615
+ rest = rest.slice(bare[0].length);
1616
+ continue;
1617
+ }
1618
+ } else if (rest.startsWith(".")) {
1619
+ rest = rest.slice(1);
1620
+ if (rest.startsWith("*")) {
1621
+ segments.push({ kind: "wildcard", recursive });
1622
+ rest = rest.slice(1);
1623
+ continue;
1624
+ }
1625
+ const bare = rest.match(/^([A-Za-z_][\w-]*)/);
1626
+ if (bare) {
1627
+ segments.push({ kind: "child", name: bare[1], recursive });
1628
+ rest = rest.slice(bare[0].length);
1629
+ continue;
1630
+ }
1631
+ throw new OverlayError(`Invalid JSONPath segment after '.' in '${path}'`, { target: path });
1632
+ }
1633
+ if (!rest.startsWith("[")) {
1634
+ throw new OverlayError(`Invalid JSONPath segment at '${rest}' in '${path}'`, { target: path });
1635
+ }
1636
+ const bracket = matchBracket(rest, path);
1637
+ const inner = bracket.inner.trim();
1638
+ rest = bracket.rest;
1639
+ if (inner === "*") {
1640
+ segments.push({ kind: "wildcard", recursive });
1641
+ } else if (/^-?\d+$/.test(inner)) {
1642
+ segments.push({ kind: "index", index: parseInt(inner, 10), recursive });
1643
+ } else if (/^'.*'$/.test(inner) || /^".*"$/.test(inner)) {
1644
+ segments.push({ kind: "child", name: inner.slice(1, -1), recursive });
1645
+ } else if (inner.startsWith("?(") && inner.endsWith(")")) {
1646
+ segments.push(parseFilter(inner.slice(2, -1).trim(), path, recursive));
1647
+ } else {
1648
+ throw new OverlayError(`Unsupported JSONPath selector '[${inner}]' in '${path}'`, { target: path });
1649
+ }
1650
+ }
1651
+ return segments;
1652
+ }
1653
+ function matchBracket(input, fullPath) {
1654
+ let quote = null;
1655
+ let depth = 0;
1656
+ for (let i = 1; i < input.length; i++) {
1657
+ const char = input[i];
1658
+ if (quote) {
1659
+ if (char === quote) quote = null;
1660
+ } else if (char === "'" || char === '"') {
1661
+ quote = char;
1662
+ } else if (char === "[") {
1663
+ depth++;
1664
+ } else if (char === "]") {
1665
+ if (depth === 0) {
1666
+ return { inner: input.slice(1, i), rest: input.slice(i + 1) };
1667
+ }
1668
+ depth--;
1669
+ }
1670
+ }
1671
+ throw new OverlayError(`Unterminated '[' selector in '${fullPath}'`, { target: fullPath });
1672
+ }
1673
+ function parseFilter(expr, path, recursive) {
1674
+ const match = expr.match(/^@(?:\.([A-Za-z_][\w-]*)|\['([^']*)'\]|\["([^"]*)"\])\s*(?:(==|!=)\s*(.+))?$/);
1675
+ if (!match) {
1676
+ throw new OverlayError(`Unsupported filter expression '?(${expr})' in '${path}'`, { target: path });
1677
+ }
1678
+ const field = match[1] ?? match[2] ?? match[3];
1679
+ const op = match[4];
1680
+ if (!op) {
1681
+ return { kind: "filter", field, op: "exists", recursive };
1682
+ }
1683
+ const raw = match[5].trim();
1684
+ let literal;
1685
+ if (/^'.*'$/.test(raw) || /^".*"$/.test(raw)) {
1686
+ literal = raw.slice(1, -1);
1687
+ } else if (/^-?\d+(\.\d+)?$/.test(raw)) {
1688
+ literal = parseFloat(raw);
1689
+ } else if (raw === "true" || raw === "false") {
1690
+ literal = raw === "true";
1691
+ } else {
1692
+ throw new OverlayError(`Unsupported filter literal '${raw}' in '${path}'`, { target: path });
1693
+ }
1694
+ return { kind: "filter", field, op, literal, recursive };
1695
+ }
1696
+ function isContainer(value) {
1697
+ return value !== null && typeof value === "object";
1698
+ }
1699
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1700
+ function descendants(match) {
1701
+ const result = [];
1702
+ const walk = (node) => {
1703
+ if (!isContainer(node)) return;
1704
+ if (Array.isArray(node)) {
1705
+ node.forEach((item, index) => {
1706
+ result.push({ parent: node, key: index, value: item });
1707
+ walk(item);
1708
+ });
1709
+ } else {
1710
+ for (const [key, value] of Object.entries(node)) {
1711
+ result.push({ parent: node, key, value });
1712
+ walk(value);
1713
+ }
1714
+ }
1715
+ };
1716
+ walk(match.value);
1717
+ return result;
1718
+ }
1719
+ function dedupeMatches(matches) {
1720
+ const seen = /* @__PURE__ */ new Map();
1721
+ const result = [];
1722
+ for (const match of matches) {
1723
+ let keys = seen.get(match.parent);
1724
+ if (!keys) {
1725
+ keys = /* @__PURE__ */ new Set();
1726
+ seen.set(match.parent, keys);
1727
+ }
1728
+ if (keys.has(match.key)) continue;
1729
+ keys.add(match.key);
1730
+ result.push(match);
1731
+ }
1732
+ return result;
1733
+ }
1734
+ function applySegment(matches, segment) {
1735
+ const scope = segment.recursive ? matches.flatMap((m) => [m, ...descendants(m)]) : matches;
1736
+ const next = [];
1737
+ for (const match of scope) {
1738
+ const node = match.value;
1739
+ switch (segment.kind) {
1740
+ case "child": {
1741
+ if (isContainer(node) && !Array.isArray(node) && !UNSAFE_KEYS.has(segment.name) && Object.prototype.hasOwnProperty.call(node, segment.name)) {
1742
+ next.push({ parent: node, key: segment.name, value: node[segment.name] });
1743
+ }
1744
+ break;
1745
+ }
1746
+ case "wildcard": {
1747
+ if (Array.isArray(node)) {
1748
+ node.forEach((item, index) => next.push({ parent: node, key: index, value: item }));
1749
+ } else if (isContainer(node)) {
1750
+ for (const [key, value] of Object.entries(node)) {
1751
+ next.push({ parent: node, key, value });
1752
+ }
1753
+ }
1754
+ break;
1755
+ }
1756
+ case "index": {
1757
+ if (Array.isArray(node)) {
1758
+ const index = segment.index < 0 ? node.length + segment.index : segment.index;
1759
+ if (index >= 0 && index < node.length) {
1760
+ next.push({ parent: node, key: index, value: node[index] });
1761
+ }
1762
+ }
1763
+ break;
1764
+ }
1765
+ case "filter": {
1766
+ const members = Array.isArray(node) ? node.map((item, index) => ({ parent: node, key: index, value: item })) : isContainer(node) ? Object.entries(node).map(([key, value]) => ({ parent: node, key, value })) : [];
1767
+ for (const member of members) {
1768
+ if (!isContainer(member.value) || Array.isArray(member.value)) continue;
1769
+ const fieldValue = member.value[segment.field];
1770
+ const keep = segment.op === "exists" ? fieldValue !== void 0 : segment.op === "==" ? fieldValue === segment.literal : fieldValue !== segment.literal;
1771
+ if (keep) next.push(member);
1772
+ }
1773
+ break;
1774
+ }
1775
+ }
1776
+ }
1777
+ return next;
1778
+ }
1779
+ function deepMerge(target, update) {
1780
+ for (const [key, value] of Object.entries(update)) {
1781
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
1782
+ const existing = target[key];
1783
+ if (isContainer(value) && !Array.isArray(value) && isContainer(existing) && !Array.isArray(existing)) {
1784
+ deepMerge(existing, value);
1785
+ } else {
1786
+ target[key] = value;
1787
+ }
1788
+ }
1789
+ }
1790
+ function applyOverlay(document, overlay) {
1791
+ if (!overlay || typeof overlay !== "object" || !Array.isArray(overlay.actions)) {
1792
+ throw new OverlayError("Overlay document must have an actions array", {});
1793
+ }
1794
+ const result = JSON.parse(JSON.stringify(document));
1795
+ for (const [index, action] of overlay.actions.entries()) {
1796
+ if (!action || typeof action !== "object" || typeof action.target !== "string") {
1797
+ throw new OverlayError(`Overlay action #${index} must have a string target`, { index });
1798
+ }
1799
+ if (action.update === void 0 && action.remove !== true) {
1800
+ throw new OverlayError(`Overlay action #${index} needs 'update' or 'remove: true'`, {
1801
+ index,
1802
+ target: action.target
1803
+ });
1804
+ }
1805
+ const segments = parsePath(action.target);
1806
+ let matches = [{ parent: null, key: null, value: result }];
1807
+ for (const segment of segments) {
1808
+ matches = dedupeMatches(applySegment(matches, segment));
1809
+ }
1810
+ if (action.remove === true) {
1811
+ const arrayRemovals = /* @__PURE__ */ new Map();
1812
+ for (const match of matches) {
1813
+ if (match.parent === null) {
1814
+ throw new OverlayError("Overlay cannot remove the document root", { target: action.target });
1815
+ }
1816
+ if (Array.isArray(match.parent)) {
1817
+ const indices = arrayRemovals.get(match.parent) ?? [];
1818
+ indices.push(match.key);
1819
+ arrayRemovals.set(match.parent, indices);
1820
+ } else {
1821
+ delete match.parent[match.key];
1822
+ }
1823
+ }
1824
+ for (const [parent, indices] of arrayRemovals) {
1825
+ for (const index2 of indices.sort((a, b) => b - a)) {
1826
+ parent.splice(index2, 1);
1827
+ }
1828
+ }
1829
+ continue;
1830
+ }
1831
+ for (const match of matches) {
1832
+ const node = match.value;
1833
+ if (Array.isArray(node)) {
1834
+ node.push(action.update);
1835
+ } else if (isContainer(node) && isContainer(action.update) && !Array.isArray(action.update)) {
1836
+ deepMerge(node, action.update);
1837
+ } else {
1838
+ if (match.parent === null) {
1839
+ throw new OverlayError("Overlay cannot replace the document root with a non-object", {
1840
+ target: action.target
1841
+ });
1842
+ }
1843
+ match.parent[match.key] = action.update;
1844
+ }
1845
+ }
1846
+ }
1847
+ return result;
1848
+ }
1849
+
1850
+ // src/lint.ts
1851
+ var METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
1852
+ var PAGINATION_PARAM = /^(page|limit|offset|cursor|per_page|pagesize|page_size|after|before)$/i;
1853
+ var DEEP_SCHEMA_THRESHOLD = 8;
1854
+ var WIDE_SCHEMA_THRESHOLD = 30;
1855
+ function measureSchema(node, seen = /* @__PURE__ */ new Map()) {
1856
+ if (node === null || typeof node !== "object") {
1857
+ return { depth: 0, widestObject: 0, hasArray: false };
1858
+ }
1859
+ if (seen.has(node)) {
1860
+ return seen.get(node) ?? { depth: 0, widestObject: 0, hasArray: false };
1861
+ }
1862
+ seen.set(node, null);
1863
+ const record = node;
1864
+ let childDepth = 0;
1865
+ let widestObject = 0;
1866
+ let hasArray = record["type"] === "array" || Array.isArray(record["type"]) && record["type"].includes("array");
1867
+ const visit = (child) => {
1868
+ const shape2 = measureSchema(child, seen);
1869
+ childDepth = Math.max(childDepth, shape2.depth);
1870
+ widestObject = Math.max(widestObject, shape2.widestObject);
1871
+ hasArray = hasArray || shape2.hasArray;
1872
+ };
1873
+ const properties = record["properties"];
1874
+ if (properties && typeof properties === "object") {
1875
+ widestObject = Math.max(widestObject, Object.keys(properties).length);
1876
+ for (const child of Object.values(properties)) visit(child);
1877
+ }
1878
+ for (const key of ["items", "additionalProperties", "not", "contentSchema"]) {
1879
+ const value = record[key];
1880
+ if (value && typeof value === "object" && !Array.isArray(value)) visit(value);
1881
+ if (Array.isArray(value)) value.forEach(visit);
1882
+ }
1883
+ for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
1884
+ const value = record[key];
1885
+ if (Array.isArray(value)) value.forEach(visit);
1886
+ }
1887
+ const shape = { depth: childDepth + 1, widestObject, hasArray };
1888
+ seen.set(node, shape);
1889
+ return shape;
1890
+ }
1891
+ function schemaHasExample(node, seen = /* @__PURE__ */ new Set()) {
1892
+ if (node === null || typeof node !== "object" || seen.has(node)) return false;
1893
+ seen.add(node);
1894
+ const record = node;
1895
+ if (record["example"] !== void 0 || record["examples"] !== void 0) return true;
1896
+ const properties = record["properties"];
1897
+ if (properties && typeof properties === "object") {
1898
+ if (Object.values(properties).some((child) => schemaHasExample(child, seen))) return true;
1899
+ }
1900
+ for (const key of ["items", "additionalProperties", "not", "contentSchema"]) {
1901
+ const value = record[key];
1902
+ if (value && typeof value === "object" && !Array.isArray(value) && schemaHasExample(value, seen)) return true;
1903
+ if (Array.isArray(value) && value.some((item) => schemaHasExample(item, seen))) return true;
1904
+ }
1905
+ for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
1906
+ const value = record[key];
1907
+ if (Array.isArray(value) && value.some((member) => schemaHasExample(member, seen))) return true;
1908
+ }
1909
+ return false;
1910
+ }
1911
+ function hasAnyExample(content) {
1912
+ if (!content) return false;
1913
+ return Object.values(content).some((media) => {
1914
+ if (!media || typeof media !== "object") return false;
1915
+ const record = media;
1916
+ if (record["example"] !== void 0 || record["examples"] !== void 0) return true;
1917
+ return schemaHasExample(record["schema"]);
1918
+ });
1919
+ }
1920
+ function lintDocument(document) {
1921
+ const findings = [];
1922
+ const operationIds = /* @__PURE__ */ new Map();
1923
+ const paths = document.paths ?? {};
1924
+ for (const [pathStr, pathItem] of Object.entries(paths).sort(([a], [b]) => a < b ? -1 : 1)) {
1925
+ if (!pathItem || "$ref" in pathItem) continue;
1926
+ const pathLevelParameters = (pathItem["parameters"] ?? []).filter(
1927
+ (param) => !isReferenceObject(param)
1928
+ );
1929
+ for (const method of METHODS) {
1930
+ const operation = pathItem[method];
1931
+ if (!operation) continue;
1932
+ const label = `${method.toUpperCase()} ${pathStr}`;
1933
+ if (!operation.operationId) {
1934
+ findings.push({
1935
+ severity: "warning",
1936
+ code: "missing-operation-id",
1937
+ message: "Operation has no operationId; the tool name will be generated from the method and path.",
1938
+ path: label,
1939
+ hint: "Add a short, action-oriented operationId (it becomes the tool name)."
1940
+ });
1941
+ } else {
1942
+ const existing = operationIds.get(operation.operationId) ?? [];
1943
+ existing.push(label);
1944
+ operationIds.set(operation.operationId, existing);
1945
+ if (operation.operationId.length > 64) {
1946
+ findings.push({
1947
+ severity: "info",
1948
+ code: "long-operation-id",
1949
+ message: `operationId '${operation.operationId.slice(0, 40)}\u2026' exceeds 64 characters and will be truncated with a hash suffix.`,
1950
+ path: label,
1951
+ hint: "Shorten the operationId below 64 characters to keep tool names readable."
1952
+ });
1953
+ }
1954
+ }
1955
+ const prose = `${operation.summary ?? ""} ${operation.description ?? ""}`.trim();
1956
+ if (prose.length === 0) {
1957
+ findings.push({
1958
+ severity: "warning",
1959
+ code: "missing-description",
1960
+ message: "Operation has neither summary nor description; the model only sees the method and path.",
1961
+ path: label,
1962
+ hint: "Describe WHEN to use this operation and what it returns (or patch it in with an overlay)."
1963
+ });
1964
+ } else if (prose.length < 20) {
1965
+ findings.push({
1966
+ severity: "info",
1967
+ code: "vague-description",
1968
+ message: `Operation description is only ${prose.length} characters \u2014 likely too vague for reliable tool selection.`,
1969
+ path: label,
1970
+ hint: "Expand the description with the use case and key parameters."
1971
+ });
1972
+ }
1973
+ const parameters = [
1974
+ ...pathLevelParameters,
1975
+ ...(operation.parameters ?? []).filter((param) => !isReferenceObject(param))
1976
+ ];
1977
+ const undescribed = parameters.filter((param) => !param.description).map((param) => param.name);
1978
+ if (undescribed.length > 0) {
1979
+ findings.push({
1980
+ severity: "info",
1981
+ code: "missing-parameter-description",
1982
+ message: `Parameter(s) without description: ${undescribed.join(", ")}.`,
1983
+ path: label,
1984
+ hint: "Describe each parameter \u2014 models mis-fill undocumented arguments."
1985
+ });
1986
+ }
1987
+ const responses = operation.responses ?? {};
1988
+ const successCodes = Object.keys(responses).filter((code) => /^2(\d\d|XX)$/i.test(code));
1989
+ if (successCodes.length === 0 && !responses["default"]) {
1990
+ findings.push({
1991
+ severity: "warning",
1992
+ code: "missing-success-response",
1993
+ message: "Operation declares no 2xx or default response; no output schema can be generated.",
1994
+ path: label,
1995
+ hint: "Add the success response with its schema."
1996
+ });
1997
+ }
1998
+ let responseShape = { depth: 0, widestObject: 0, hasArray: false };
1999
+ for (const code of [...successCodes, "default"]) {
2000
+ const response = responses[code];
2001
+ if (!response || typeof response !== "object" || isReferenceObject(response)) continue;
2002
+ const content = response["content"];
2003
+ if (!content) continue;
2004
+ for (const media of Object.values(content)) {
2005
+ const schema = media && typeof media === "object" ? media["schema"] : void 0;
2006
+ const shape = measureSchema(schema);
2007
+ responseShape = {
2008
+ depth: Math.max(responseShape.depth, shape.depth),
2009
+ widestObject: Math.max(responseShape.widestObject, shape.widestObject),
2010
+ hasArray: responseShape.hasArray || shape.hasArray
2011
+ };
2012
+ }
2013
+ }
2014
+ if (method === "get" && responseShape.hasArray) {
2015
+ const hasPagination = parameters.some((param) => param.in === "query" && PAGINATION_PARAM.test(param.name));
2016
+ if (!hasPagination) {
2017
+ findings.push({
2018
+ severity: "warning",
2019
+ code: "unpaginated-list",
2020
+ message: "GET returns an array but declares no pagination parameter \u2014 responses can blow past client result limits (Claude Code caps tool results at 25K tokens).",
2021
+ path: label,
2022
+ hint: "Add limit/cursor/page parameters, or shape responses at the server."
2023
+ });
2024
+ }
2025
+ }
2026
+ const body = operation.requestBody;
2027
+ const bodyContent = body && !isReferenceObject(body) ? body.content : void 0;
2028
+ let requestShape = { depth: 0, widestObject: 0, hasArray: false };
2029
+ for (const media of Object.values(bodyContent ?? {})) {
2030
+ const schema = media && typeof media === "object" ? media["schema"] : void 0;
2031
+ const shape = measureSchema(schema);
2032
+ requestShape = {
2033
+ depth: Math.max(requestShape.depth, shape.depth),
2034
+ widestObject: Math.max(requestShape.widestObject, shape.widestObject),
2035
+ hasArray: requestShape.hasArray || shape.hasArray
2036
+ };
2037
+ }
2038
+ const maxDepth = Math.max(requestShape.depth, responseShape.depth);
2039
+ if (maxDepth > DEEP_SCHEMA_THRESHOLD) {
2040
+ findings.push({
2041
+ severity: "warning",
2042
+ code: "deep-schema",
2043
+ message: `Schema nesting reaches depth ${maxDepth} (threshold ${DEEP_SCHEMA_THRESHOLD}) \u2014 deep schemas cost tokens and reduce accuracy.`,
2044
+ path: label,
2045
+ hint: "Flatten the schema, or bound generation with maxSchemaDepth."
2046
+ });
2047
+ }
2048
+ const maxWidth = Math.max(requestShape.widestObject, responseShape.widestObject);
2049
+ if (maxWidth > WIDE_SCHEMA_THRESHOLD) {
2050
+ findings.push({
2051
+ severity: "info",
2052
+ code: "wide-schema",
2053
+ message: `An object schema declares ${maxWidth} properties (threshold ${WIDE_SCHEMA_THRESHOLD}).`,
2054
+ path: label,
2055
+ hint: "Split the payload, or bound generation with maxProperties."
2056
+ });
2057
+ }
2058
+ if (bodyContent && !hasAnyExample(bodyContent)) {
2059
+ findings.push({
2060
+ severity: "info",
2061
+ code: "missing-request-example",
2062
+ message: "Request body has no example \u2014 examples measurably improve complex-parameter accuracy.",
2063
+ path: label,
2064
+ hint: "Add a media-type example (and enable includeExamples), or patch one in with an overlay."
2065
+ });
2066
+ }
716
2067
  }
717
2068
  }
718
- };
719
- var LoadError = class extends OpenAPIToolError {
720
- constructor(message, context) {
721
- super(message, context);
2069
+ for (const [operationId, labels] of operationIds) {
2070
+ if (labels.length > 1) {
2071
+ findings.push({
2072
+ severity: "error",
2073
+ code: "duplicate-operation-id",
2074
+ message: `operationId '${operationId}' is used by ${labels.length} operations: ${labels.join(", ")}.`,
2075
+ path: labels[0],
2076
+ hint: "Make operationIds unique \u2014 duplicates force hash-suffixed tool names."
2077
+ });
2078
+ }
722
2079
  }
723
- };
724
- var SsrfError = class extends LoadError {
725
- constructor(message, context) {
726
- super(message, context);
2080
+ const rank = { error: 0, warning: 1, info: 2 };
2081
+ findings.sort(
2082
+ (a, b) => rank[a.severity] - rank[b.severity] || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0) || (a.code < b.code ? -1 : 1)
2083
+ );
2084
+ return {
2085
+ findings,
2086
+ counts: {
2087
+ error: findings.filter((f) => f.severity === "error").length,
2088
+ warning: findings.filter((f) => f.severity === "warning").length,
2089
+ info: findings.filter((f) => f.severity === "info").length
2090
+ }
2091
+ };
2092
+ }
2093
+
2094
+ // src/validator.ts
2095
+ var Validator = class {
2096
+ /**
2097
+ * Validate an OpenAPI document
2098
+ */
2099
+ async validate(document) {
2100
+ const errors = [];
2101
+ const warnings = [];
2102
+ if (!document.openapi) {
2103
+ errors.push({
2104
+ message: "Missing required field: openapi",
2105
+ path: "/openapi",
2106
+ code: "MISSING_OPENAPI_VERSION"
2107
+ });
2108
+ } else if (!this.isValidOpenAPIVersion(document.openapi)) {
2109
+ errors.push({
2110
+ message: `Unsupported OpenAPI version: ${document.openapi}. Expected 3.0.x or 3.1.x`,
2111
+ path: "/openapi",
2112
+ code: "INVALID_OPENAPI_VERSION"
2113
+ });
2114
+ }
2115
+ if (!document.info) {
2116
+ errors.push({
2117
+ message: "Missing required field: info",
2118
+ path: "/info",
2119
+ code: "MISSING_INFO"
2120
+ });
2121
+ } else {
2122
+ if (!document.info.title) {
2123
+ errors.push({
2124
+ message: "Missing required field: info.title",
2125
+ path: "/info/title",
2126
+ code: "MISSING_TITLE"
2127
+ });
2128
+ }
2129
+ if (!document.info.version) {
2130
+ errors.push({
2131
+ message: "Missing required field: info.version",
2132
+ path: "/info/version",
2133
+ code: "MISSING_VERSION"
2134
+ });
2135
+ }
2136
+ }
2137
+ if (!document.paths || Object.keys(document.paths).length === 0) {
2138
+ warnings.push({
2139
+ message: "No paths defined in OpenAPI document",
2140
+ path: "/paths",
2141
+ code: "NO_PATHS"
2142
+ });
2143
+ } else {
2144
+ this.validatePaths(document.paths, errors, warnings);
2145
+ }
2146
+ if (!document.servers || document.servers.length === 0) {
2147
+ warnings.push({
2148
+ message: "No servers defined. You may need to provide a baseUrl option.",
2149
+ path: "/servers",
2150
+ code: "NO_SERVERS"
2151
+ });
2152
+ }
2153
+ if (document.security && !document.components?.securitySchemes) {
2154
+ warnings.push({
2155
+ message: "Security requirements defined but no security schemes found",
2156
+ path: "/security",
2157
+ code: "NO_SECURITY_SCHEMES"
2158
+ });
2159
+ }
2160
+ return {
2161
+ valid: errors.length === 0,
2162
+ errors: errors.length > 0 ? errors : void 0,
2163
+ warnings: warnings.length > 0 ? warnings : void 0
2164
+ };
727
2165
  }
728
- };
729
- var ParseError = class extends OpenAPIToolError {
730
- constructor(message, context) {
731
- super(message, context);
2166
+ /**
2167
+ * Check if OpenAPI version is valid
2168
+ */
2169
+ isValidOpenAPIVersion(version) {
2170
+ return /^3\.[01]\.\d+$/.test(version);
732
2171
  }
733
- };
734
- var ValidationError = class extends OpenAPIToolError {
735
- errors;
736
- constructor(message, context) {
737
- super(message, context);
738
- this.errors = context?.["errors"];
2172
+ /**
2173
+ * Validate paths
2174
+ */
2175
+ validatePaths(paths, errors, warnings) {
2176
+ for (const [path, pathItem] of Object.entries(paths)) {
2177
+ if (!pathItem) continue;
2178
+ if (!path.startsWith("/")) {
2179
+ errors.push({
2180
+ message: `Path must start with '/': ${path}`,
2181
+ path: `/paths/${path}`,
2182
+ code: "INVALID_PATH_FORMAT"
2183
+ });
2184
+ }
2185
+ const methods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
2186
+ let hasOperations = false;
2187
+ for (const method of methods) {
2188
+ const operation = pathItem[method];
2189
+ if (operation) {
2190
+ hasOperations = true;
2191
+ this.validateOperation(operation, path, method, errors, warnings);
2192
+ }
2193
+ }
2194
+ if (!hasOperations && !pathItem.$ref) {
2195
+ warnings.push({
2196
+ message: `Path has no operations: ${path}`,
2197
+ path: `/paths/${path}`,
2198
+ code: "NO_OPERATIONS"
2199
+ });
2200
+ }
2201
+ }
739
2202
  }
740
- };
741
- var GenerationError = class extends OpenAPIToolError {
742
- constructor(message, context) {
743
- super(message, context);
2203
+ /**
2204
+ * Validate an operation
2205
+ */
2206
+ validateOperation(operation, path, method, errors, warnings) {
2207
+ const basePath = `/paths/${path}/${method}`;
2208
+ if (!operation.operationId) {
2209
+ warnings.push({
2210
+ message: `Operation missing operationId: ${method.toUpperCase()} ${path}`,
2211
+ path: `${basePath}/operationId`,
2212
+ code: "NO_OPERATION_ID"
2213
+ });
2214
+ }
2215
+ if (!operation.responses || Object.keys(operation.responses).length === 0) {
2216
+ errors.push({
2217
+ message: `Operation missing responses: ${method.toUpperCase()} ${path}`,
2218
+ path: `${basePath}/responses`,
2219
+ code: "NO_RESPONSES"
2220
+ });
2221
+ }
2222
+ if (operation.parameters) {
2223
+ this.validateParameters(operation.parameters, path, method, errors, warnings);
2224
+ }
2225
+ const pathParams = path.match(/\{([^{}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
2226
+ const definedPathParams = new Set(
2227
+ operation.parameters?.filter((p) => p.in === "path").map((p) => p.name) ?? []
2228
+ );
2229
+ for (const param of pathParams) {
2230
+ if (!definedPathParams.has(param)) {
2231
+ errors.push({
2232
+ message: `Path parameter '${param}' not defined in parameters: ${method.toUpperCase()} ${path}`,
2233
+ path: `${basePath}/parameters`,
2234
+ code: "MISSING_PATH_PARAMETER"
2235
+ });
2236
+ }
2237
+ }
744
2238
  }
745
- };
746
- var SchemaError = class extends OpenAPIToolError {
747
- constructor(message, context) {
748
- super(message, context);
2239
+ /**
2240
+ * Validate parameters
2241
+ */
2242
+ validateParameters(parameters, path, method, errors, warnings) {
2243
+ const basePath = `/paths/${path}/${method}/parameters`;
2244
+ for (let i = 0; i < parameters.length; i++) {
2245
+ const param = parameters[i];
2246
+ const paramPath = `${basePath}/${i}`;
2247
+ if (!param.name) {
2248
+ errors.push({
2249
+ message: "Parameter missing name",
2250
+ path: `${paramPath}/name`,
2251
+ code: "MISSING_PARAMETER_NAME"
2252
+ });
2253
+ }
2254
+ if (!param.in) {
2255
+ errors.push({
2256
+ message: 'Parameter missing "in" field',
2257
+ path: `${paramPath}/in`,
2258
+ code: "MISSING_PARAMETER_IN"
2259
+ });
2260
+ } else if (!["path", "query", "header", "cookie"].includes(param.in)) {
2261
+ errors.push({
2262
+ message: `Invalid parameter location: ${param.in}`,
2263
+ path: `${paramPath}/in`,
2264
+ code: "INVALID_PARAMETER_IN"
2265
+ });
2266
+ }
2267
+ if (param.in === "path" && !param.required) {
2268
+ errors.push({
2269
+ message: `Path parameter '${param.name}' must be required`,
2270
+ path: `${paramPath}/required`,
2271
+ code: "PATH_PARAMETER_NOT_REQUIRED"
2272
+ });
2273
+ }
2274
+ if (!param.schema && !param.content) {
2275
+ errors.push({
2276
+ message: `Parameter '${param.name}' missing schema or content`,
2277
+ path: `${paramPath}`,
2278
+ code: "MISSING_PARAMETER_SCHEMA"
2279
+ });
2280
+ }
2281
+ }
749
2282
  }
750
2283
  };
751
2284
 
@@ -1098,797 +2631,835 @@ async function selectTransport(opts, url) {
1098
2631
  throw new SsrfError("No fetch implementation available to load OpenAPI spec from URL", { url });
1099
2632
  }
1100
2633
  return nodePinnedTransport(modules);
1101
- }
1102
- async function safeFetch(url, opts) {
1103
- const { headers, timeoutMs = 3e4, followRedirects = true, maxRedirects = 5, ssrf, lookup } = opts;
1104
- const transport = await selectTransport(opts, url);
1105
- let current = url;
1106
- for (let hop = 0; hop <= maxRedirects; hop++) {
1107
- const pinned = await assertUrlSafe(current, ssrf, lookup);
1108
- const controller = new AbortController();
1109
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1110
- let response;
1111
- try {
1112
- response = await transport(current, { headers, signal: controller.signal, pinned, maxBytes: opts.maxResponseBytes });
1113
- } finally {
1114
- clearTimeout(timer);
1115
- }
1116
- const status = typeof response.status === "number" ? response.status : 0;
1117
- const isRedirect = status >= 300 && status < 400 && status !== 304;
1118
- if (!isRedirect || !followRedirects) {
1119
- return response;
1120
- }
1121
- const location = response.headers?.get?.("location") ?? void 0;
1122
- if (!location) {
1123
- return response;
1124
- }
1125
- current = new URL(location, current).toString();
1126
- }
1127
- throw new SsrfError(`Too many redirects while loading OpenAPI spec (max ${maxRedirects})`, { url });
1128
- }
1129
-
1130
- // src/generator.ts
1131
- var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
1132
- document;
1133
- dereferencedDocument;
1134
- options;
1135
- /**
1136
- * Private constructor - use static factory methods to create instances
1137
- */
1138
- constructor(document, options = {}) {
1139
- this.document = document;
1140
- this.options = {
1141
- dereference: options.dereference ?? true,
1142
- baseUrl: options.baseUrl ?? "",
1143
- headers: options.headers ?? {},
1144
- timeout: options.timeout ?? 3e4,
1145
- validate: options.validate ?? true,
1146
- followRedirects: options.followRedirects ?? true,
1147
- refResolution: options.refResolution ?? {}
1148
- };
1149
- }
1150
- /**
1151
- * Create generator from a URL
1152
- */
1153
- static async fromURL(url, options = {}) {
1154
- try {
1155
- const response = await safeFetch(url, {
1156
- headers: options.headers,
1157
- timeoutMs: options.timeout ?? 3e4,
1158
- followRedirects: options.followRedirects ?? true,
1159
- ssrf: normalizeSsrfOptions(options.refResolution)
1160
- });
1161
- if (!response.ok) {
1162
- throw new LoadError(`Failed to fetch OpenAPI spec from URL: ${response.status} ${response.statusText}`, {
1163
- url,
1164
- status: response.status
1165
- });
1166
- }
1167
- const contentType = response.headers.get("content-type") || "";
1168
- const text = await response.text();
1169
- let document;
1170
- if (contentType.includes("yaml") || contentType.includes("yml") || url.match(/\.ya?ml$/i)) {
1171
- document = yaml.parse(text);
1172
- } else {
1173
- document = JSON.parse(text);
1174
- }
1175
- return new _OpenAPIToolGenerator(document, options);
1176
- } catch (error) {
1177
- if (error instanceof LoadError) {
1178
- throw error;
1179
- }
1180
- const errorMessage = error instanceof Error ? error.message : String(error);
1181
- throw new LoadError(`Failed to load OpenAPI spec from URL: ${errorMessage}`, {
1182
- url,
1183
- originalError: error
1184
- });
1185
- }
1186
- }
1187
- /**
1188
- * Create generator from a file path
1189
- */
1190
- static async fromFile(filePath, options = {}) {
1191
- try {
1192
- const [path, fs] = await Promise.all([import("path"), import("fs/promises")]);
1193
- const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
1194
- const content = await fs.readFile(absolutePath, "utf-8");
1195
- const ext = path.extname(filePath).toLowerCase();
1196
- let document;
1197
- if (ext === ".yaml" || ext === ".yml") {
1198
- document = yaml.parse(content);
1199
- } else if (ext === ".json") {
1200
- document = JSON.parse(content);
1201
- } else {
1202
- try {
1203
- document = JSON.parse(content);
1204
- } catch {
1205
- document = yaml.parse(content);
1206
- }
1207
- }
1208
- return new _OpenAPIToolGenerator(document, options);
1209
- } catch (error) {
1210
- const errorMessage = error instanceof Error ? error.message : String(error);
1211
- throw new LoadError(`Failed to load OpenAPI spec from file: ${errorMessage}`, {
1212
- filePath,
1213
- originalError: error
1214
- });
1215
- }
1216
- }
1217
- /**
1218
- * Create generator from a YAML string
1219
- */
1220
- static async fromYAML(yamlString, options = {}) {
1221
- try {
1222
- const document = yaml.parse(yamlString);
1223
- return new _OpenAPIToolGenerator(document, options);
1224
- } catch (error) {
1225
- const errorMessage = error instanceof Error ? error.message : String(error);
1226
- throw new ParseError(`Failed to parse YAML: ${errorMessage}`, {
1227
- originalError: error
1228
- });
1229
- }
1230
- }
1231
- /**
1232
- * Create generator from a JSON object
1233
- */
1234
- static async fromJSON(json, options = {}) {
1235
- const document = JSON.parse(JSON.stringify(json));
1236
- return new _OpenAPIToolGenerator(document, options);
1237
- }
1238
- /**
1239
- * Get the OpenAPI document
1240
- */
1241
- getDocument() {
1242
- return this.dereferencedDocument ?? this.document;
1243
- }
1244
- /**
1245
- * Validate the OpenAPI document
1246
- */
1247
- async validate() {
1248
- const validator = new Validator();
1249
- return validator.validate(this.document);
1250
- }
1251
- // NOTE: internal/private-address blocking + IPv4-mapped-IPv6 decoding now live
1252
- // in `ssrf.ts` (`isBlockedHostname` / `isBlockedAddress` / `decodeIpv4MappedIpv6`),
1253
- // shared by the spec-URL fetch (`fromURL`) and the `$ref` resolver below, and
1254
- // augmented there with DNS resolution (closing the DNS-name-to-internal bypass)
1255
- // and per-hop redirect re-validation (`safeFetch`).
1256
- /**
1257
- * Build $RefParser options based on refResolution configuration.
1258
- * Defaults: allow http/https, block file://, block internal IPs.
1259
- */
1260
- buildRefParserOptions() {
1261
- const raw = this.options.refResolution;
1262
- const refOpts = {
1263
- allowedProtocols: raw.allowedProtocols ?? ["http", "https"],
1264
- allowedHosts: raw.allowedHosts ?? [],
1265
- blockedHosts: raw.blockedHosts ?? [],
1266
- allowInternalIPs: raw.allowInternalIPs ?? false
1267
- };
1268
- const allowedProtocols = new Set(refOpts.allowedProtocols);
1269
- const hasNetworkProtocol = allowedProtocols.size > 0 && !([...allowedProtocols].length === 1 && allowedProtocols.has("file"));
1270
- if (allowedProtocols.size === 0) {
1271
- return { resolve: { external: false } };
1272
- }
1273
- const resolveConfig = {
1274
- external: true,
1275
- file: allowedProtocols.has("file") ? void 0 : false
1276
- };
1277
- if (hasNetworkProtocol) {
1278
- const hasHostAllowlist = refOpts.allowedHosts.length > 0;
1279
- const hostAllowSet = new Set(refOpts.allowedHosts);
1280
- resolveConfig["http"] = {
1281
- // SECURITY: never auto-follow HTTP redirects when resolving external
1282
- // `$ref`s. `canRead` validates only the INITIAL URL; the resolver's
1283
- // default redirect-following (up to 5 hops) re-fetches the `Location`
1284
- // target WITHOUT re-invoking `canRead`, so an allowlisted host could
1285
- // 302 → `http://169.254.169.254/...` and smuggle a blocked target past
1286
- // the allow/deny lists. `redirects: 0` refuses the first redirect, and
1287
- // our custom `read` (below) additionally refuses redirects itself.
1288
- redirects: 0,
1289
- // Synchronous gate: protocol, host allow-list, and literal/known
1290
- // internal hosts. DNS names that *resolve* to internal addresses pass
1291
- // here (canRead cannot be async) and are caught in `read` via DNS
1292
- // resolution — closing the `127.0.0.1.nip.io` bypass for `$ref`s too.
1293
- canRead: (file) => {
1294
- try {
1295
- const parsed = new URL(file.url);
1296
- const protocol = parsed.protocol.replace(":", "");
1297
- if (!allowedProtocols.has(protocol)) {
1298
- return false;
1299
- }
1300
- if (hasHostAllowlist && !hostAllowSet.has(parsed.hostname)) {
1301
- return false;
1302
- }
1303
- if (isBlockedHostname(parsed.hostname, refOpts)) {
1304
- return false;
1305
- }
1306
- return true;
1307
- } catch {
1308
- return false;
1309
- }
1310
- },
1311
- // SSRF-safe fetch: resolves DNS and rejects names that map to internal
1312
- // addresses, and refuses redirects. NOTE: deliberately does NOT forward
1313
- // `this.options.headers` (the spec-load credentials) to third-party
1314
- // `$ref` hosts — that would leak the spec's auth token cross-origin.
1315
- read: async (file) => {
1316
- const response = await safeFetch(file.url, {
1317
- timeoutMs: this.options.timeout,
1318
- followRedirects: false,
1319
- ssrf: refOpts
1320
- });
1321
- if (!response.ok) {
1322
- throw new LoadError(
1323
- `Failed to resolve external $ref "${file.url}": ${response.status} ${response.statusText}`,
1324
- { url: file.url, status: response.status }
1325
- );
1326
- }
1327
- return response.text();
1328
- }
1329
- };
1330
- } else {
1331
- resolveConfig["http"] = false;
2634
+ }
2635
+ async function safeFetch(url, opts) {
2636
+ const { headers, timeoutMs = 3e4, followRedirects = true, maxRedirects = 5, ssrf, lookup } = opts;
2637
+ const transport = await selectTransport(opts, url);
2638
+ let current = url;
2639
+ for (let hop = 0; hop <= maxRedirects; hop++) {
2640
+ const pinned = await assertUrlSafe(current, ssrf, lookup);
2641
+ const controller = new AbortController();
2642
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2643
+ let response;
2644
+ try {
2645
+ response = await transport(current, { headers, signal: controller.signal, pinned, maxBytes: opts.maxResponseBytes });
2646
+ } finally {
2647
+ clearTimeout(timer);
1332
2648
  }
1333
- return { resolve: resolveConfig };
2649
+ const status = typeof response.status === "number" ? response.status : 0;
2650
+ const isRedirect = status >= 300 && status < 400 && status !== 304;
2651
+ if (!isRedirect || !followRedirects) {
2652
+ return response;
2653
+ }
2654
+ const location = response.headers?.get?.("location") ?? void 0;
2655
+ if (!location) {
2656
+ return response;
2657
+ }
2658
+ current = new URL(location, current).toString();
1334
2659
  }
1335
- /**
1336
- * Does the document contain any EXTERNAL `$ref` (a ref that is not a local
1337
- * JSON-pointer beginning with `#`)? Only external refs require the full
1338
- * `$RefParser` (file/http resolvers, which pull Node builtins). A document
1339
- * with only internal refs can be dereferenced with the runtime-agnostic
1340
- * resolver below — so it works on V8 isolates (Cloudflare Workers) too.
1341
- */
1342
- static hasExternalRefs(node, seen = /* @__PURE__ */ new Set()) {
1343
- if (node === null || typeof node !== "object") return false;
1344
- if (seen.has(node)) return false;
1345
- seen.add(node);
1346
- if (Array.isArray(node)) return node.some((n) => _OpenAPIToolGenerator.hasExternalRefs(n, seen));
1347
- const ref = node.$ref;
1348
- if (typeof ref === "string" && !ref.startsWith("#")) return true;
1349
- return Object.values(node).some(
1350
- (v) => _OpenAPIToolGenerator.hasExternalRefs(v, seen)
1351
- );
2660
+ throw new SsrfError(`Too many redirects while loading OpenAPI spec (max ${maxRedirects})`, { url });
2661
+ }
2662
+
2663
+ // src/generator.ts
2664
+ var MCP_MAX_TOOL_NAME_LENGTH = 128;
2665
+ var DEFAULT_MAX_TOOL_NAME_LENGTH = 64;
2666
+ var MAX_NAME_DEDUP_ATTEMPTS = 256;
2667
+ function applySecureDefaults(options) {
2668
+ if (!options.secureDefaults) return options;
2669
+ return {
2670
+ ...options,
2671
+ followRedirects: options.followRedirects ?? false,
2672
+ // Merge PER KEY: a user tightening one refResolution knob (e.g.
2673
+ // blockedHosts) must not silently discard the preset's external-$ref
2674
+ // lockdown. A DEFINED allowedProtocols still wins — but an explicitly
2675
+ // undefined one (programmatic option building) must not defeat the
2676
+ // preset via object spread copying undefined-valued keys.
2677
+ refResolution: {
2678
+ ...options.refResolution,
2679
+ allowedProtocols: options.refResolution?.allowedProtocols ?? []
2680
+ }
2681
+ };
2682
+ }
2683
+ function hasUnboundedArray(node, seen = /* @__PURE__ */ new Set()) {
2684
+ if (node === null || typeof node !== "object" || seen.has(node)) return false;
2685
+ seen.add(node);
2686
+ const record = node;
2687
+ const type = record["type"];
2688
+ const isArray = type === "array" || Array.isArray(type) && type.includes("array");
2689
+ if (isArray && record["maxItems"] === void 0) return true;
2690
+ const children = [];
2691
+ const properties = record["properties"];
2692
+ if (properties && typeof properties === "object") children.push(...Object.values(properties));
2693
+ for (const key of ["items", "additionalProperties", "contentSchema"]) {
2694
+ const value = record[key];
2695
+ if (Array.isArray(value)) children.push(...value);
2696
+ else if (value && typeof value === "object") children.push(value);
1352
2697
  }
1353
- /**
1354
- * Dereference local (`#/...`) `$ref`s without `$RefParser` — pure, dependency-
1355
- * free, runtime-agnostic. A pointer cache makes circular schemas resolve to a
1356
- * shared reference instead of recursing forever (same contract as `$RefParser`).
1357
- */
1358
- static dereferenceInternal(root) {
1359
- const cache = /* @__PURE__ */ new Map();
1360
- const resolvePointer = (ptr) => {
1361
- const parts = ptr.replace(/^#\/?/, "").split("/").filter((p) => p.length > 0).map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
1362
- let cur = root;
1363
- for (const p of parts) cur = cur?.[p];
1364
- return cur;
1365
- };
1366
- const walk = (node) => {
1367
- if (node === null || typeof node !== "object") return node;
1368
- if (Array.isArray(node)) return node.map(walk);
1369
- const ref = node.$ref;
1370
- if (typeof ref === "string" && ref.startsWith("#")) {
1371
- const cached = cache.get(ref);
1372
- if (cached !== void 0) return cached;
1373
- const placeholder = {};
1374
- cache.set(ref, placeholder);
1375
- const resolved = walk(resolvePointer(ref));
1376
- if (resolved && typeof resolved === "object") Object.assign(placeholder, resolved);
1377
- return placeholder;
1378
- }
1379
- const out = {};
1380
- for (const [k, v] of Object.entries(node)) out[k] = walk(v);
1381
- return out;
1382
- };
1383
- return walk(root);
2698
+ for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
2699
+ if (Array.isArray(record[key])) children.push(...record[key]);
1384
2700
  }
1385
- /**
1386
- * Initialize the generator (dereference if needed, then validate)
1387
- */
1388
- async initialize() {
1389
- if (this.options.dereference && !this.dereferencedDocument) {
1390
- const cloned = JSON.parse(JSON.stringify(this.document));
1391
- if (!_OpenAPIToolGenerator.hasExternalRefs(cloned)) {
1392
- this.dereferencedDocument = _OpenAPIToolGenerator.dereferenceInternal(cloned);
1393
- } else {
1394
- try {
1395
- const { default: $RefParser } = await import("@apidevtools/json-schema-ref-parser");
1396
- const refParserOptions = this.buildRefParserOptions();
1397
- this.dereferencedDocument = await $RefParser.dereference(cloned, refParserOptions);
1398
- } catch (error) {
1399
- const errorMessage = error instanceof Error ? error.message : String(error);
1400
- throw new ParseError(`Failed to dereference OpenAPI document: ${errorMessage}`, {
1401
- originalError: error
1402
- });
1403
- }
2701
+ return children.some((child) => hasUnboundedArray(child, seen));
2702
+ }
2703
+ function detectResponseHints(outputSchema, mapper) {
2704
+ const paginationParams = [
2705
+ ...new Set(mapper.filter((m) => m.type === "query" && !m.security && PAGINATION_PARAM.test(m.key)).map((m) => m.key))
2706
+ ];
2707
+ const unboundedArray = outputSchema !== void 0 && hasUnboundedArray(outputSchema);
2708
+ if (!unboundedArray && paginationParams.length === 0) return void 0;
2709
+ return {
2710
+ ...unboundedArray && { unboundedArray: true },
2711
+ ...paginationParams.length > 0 && { paginationParams },
2712
+ ...unboundedArray && paginationParams.length === 0 && { largeResponseRisk: true }
2713
+ };
2714
+ }
2715
+ function composeDescription(operation, method, pathStr, strategy) {
2716
+ const fallback = `${method.toUpperCase()} ${pathStr}`;
2717
+ const summary = operation.summary?.trim();
2718
+ const description = operation.description?.trim();
2719
+ switch (strategy) {
2720
+ case "descriptionOnly":
2721
+ return description || summary || fallback;
2722
+ case "combined":
2723
+ if (summary && description && summary !== description) {
2724
+ return `${summary}
2725
+
2726
+ ${description}`;
1404
2727
  }
2728
+ return summary || description || fallback;
2729
+ case "full": {
2730
+ const parts = [];
2731
+ if (summary) parts.push(summary);
2732
+ if (description && description !== summary) parts.push(description);
2733
+ if (operation.operationId) parts.push(`Operation: ${operation.operationId}`);
2734
+ parts.push(fallback);
2735
+ return parts.join("\n\n");
1405
2736
  }
1406
- if (this.options.validate) {
1407
- const validator = new Validator();
1408
- const documentToValidate = this.dereferencedDocument ?? this.document;
1409
- const result = await validator.validate(documentToValidate);
1410
- if (!result.valid) {
1411
- throw new ParseError("Invalid OpenAPI document", { errors: result.errors });
2737
+ default:
2738
+ return summary || description || fallback;
2739
+ }
2740
+ }
2741
+ function propertyNames(schema, cap = 8) {
2742
+ const properties = schema["properties"];
2743
+ if (!properties || typeof properties !== "object") return "";
2744
+ const names = Object.keys(properties);
2745
+ const listed = names.slice(0, cap).join(", ");
2746
+ return names.length > cap ? `${listed}, \u2026` : listed;
2747
+ }
2748
+ function summarizeOutputSchema(schema) {
2749
+ const record = schema;
2750
+ const variants = record["oneOf"];
2751
+ if (Array.isArray(variants) && variants.length > 0) {
2752
+ const first = variants[0];
2753
+ const firstSummary = first && typeof first === "object" ? summarizeOutputSchema(first) : void 0;
2754
+ return firstSummary ? `${firstSummary} (${variants.length} response variants)` : void 0;
2755
+ }
2756
+ const type = record["type"];
2757
+ if (type === "object" || type === void 0 && record["properties"]) {
2758
+ const names = propertyNames(record);
2759
+ return names ? `object with fields: ${names}` : "object";
2760
+ }
2761
+ if (type === "array") {
2762
+ const items = record["items"];
2763
+ if (items && typeof items === "object" && !Array.isArray(items)) {
2764
+ const itemRecord = items;
2765
+ if (itemRecord["type"] === "object" || itemRecord["properties"]) {
2766
+ const names = propertyNames(itemRecord);
2767
+ return names ? `array of objects with fields: ${names}` : "array of objects";
2768
+ }
2769
+ if (typeof itemRecord["type"] === "string") {
2770
+ return `array of ${itemRecord["type"]}`;
1412
2771
  }
1413
2772
  }
2773
+ return "array";
1414
2774
  }
1415
- /**
1416
- * Generate all tools from the OpenAPI specification
1417
- */
1418
- async generateTools(options = {}) {
1419
- await this.initialize();
1420
- const document = this.getDocument();
1421
- const tools = [];
1422
- if (!document.paths) {
1423
- return tools;
1424
- }
1425
- for (const [pathStr, pathItem] of Object.entries(document.paths)) {
1426
- if (!pathItem || "$ref" in pathItem) continue;
1427
- const methods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
1428
- for (const method of methods) {
1429
- const operation = pathItem[method];
1430
- if (!operation) continue;
1431
- if (!this.shouldIncludeOperation(operation, pathStr, method, options)) {
1432
- continue;
1433
- }
1434
- try {
1435
- const tool = await this.generateTool(pathStr, method, options);
1436
- tools.push(tool);
1437
- } catch (error) {
1438
- const errorMessage = error instanceof Error ? error.message : String(error);
1439
- console.warn(`Failed to generate tool for ${method.toUpperCase()} ${pathStr}:`, errorMessage);
1440
- }
2775
+ if (typeof type === "string" && type !== "null") {
2776
+ return type;
2777
+ }
2778
+ return void 0;
2779
+ }
2780
+ function globToRegExp(glob) {
2781
+ let pattern = "^";
2782
+ for (let i = 0; i < glob.length; i++) {
2783
+ const char = glob[i];
2784
+ if (char === "*") {
2785
+ if (glob[i + 1] === "*") {
2786
+ pattern += ".*";
2787
+ i++;
2788
+ } else {
2789
+ pattern += "[^/]*";
1441
2790
  }
2791
+ } else if (char === "?") {
2792
+ pattern += "[^/]";
2793
+ } else {
2794
+ pattern += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2795
+ }
2796
+ }
2797
+ return new RegExp(`${pattern}$`);
2798
+ }
2799
+ function matchesAnyGlob(path, globs) {
2800
+ return globs.some((glob) => globToRegExp(glob).test(path));
2801
+ }
2802
+ function trimUnderscores(value) {
2803
+ let start = 0;
2804
+ let end = value.length;
2805
+ while (start < end && value[start] === "_") start++;
2806
+ while (end > start && value[end - 1] === "_") end--;
2807
+ return value.slice(start, end);
2808
+ }
2809
+ function fnv1aHex(input) {
2810
+ let hash = 2166136261;
2811
+ for (let i = 0; i < input.length; i++) {
2812
+ hash ^= input.charCodeAt(i);
2813
+ hash = Math.imul(hash, 16777619);
2814
+ }
2815
+ return (hash >>> 0).toString(16).padStart(8, "0");
2816
+ }
2817
+ function normalizeToolName(raw, maxLength, fallbackSeed) {
2818
+ let hashSeed = raw;
2819
+ let name = trimUnderscores(raw.replace(/[^A-Za-z0-9_.-]/g, "_").replace(/_+/g, "_"));
2820
+ if (name.length === 0) {
2821
+ hashSeed = fallbackSeed;
2822
+ name = `tool_${fnv1aHex(fallbackSeed)}`;
2823
+ }
2824
+ const cap = Math.min(Math.max(1, maxLength), MCP_MAX_TOOL_NAME_LENGTH);
2825
+ if (name.length > cap) {
2826
+ if (cap >= 13) {
2827
+ name = `${name.slice(0, cap - 9)}_${fnv1aHex(hashSeed)}`;
2828
+ } else {
2829
+ name = fnv1aHex(hashSeed).slice(0, cap);
1442
2830
  }
1443
- return tools;
1444
2831
  }
2832
+ return name;
2833
+ }
2834
+ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2835
+ document;
2836
+ dereferencedDocument;
2837
+ options;
1445
2838
  /**
1446
- * Generate a specific tool for a path and method
2839
+ * Private constructor - use static factory methods to create instances
1447
2840
  */
1448
- async generateTool(pathStr, method, options = {}) {
1449
- await this.initialize();
1450
- const document = this.getDocument();
1451
- if (!document.paths) {
1452
- throw new Error("No paths defined in OpenAPI document");
1453
- }
1454
- const pathItem = document.paths[pathStr];
1455
- const operation = pathItem?.[method.toLowerCase()];
1456
- if (!operation) {
1457
- throw new Error(`Operation not found: ${method.toUpperCase()} ${pathStr}`);
1458
- }
1459
- const parameterResolver = new ParameterResolver(options.namingStrategy);
1460
- let pathParameters = void 0;
1461
- if (pathItem.parameters) {
1462
- pathParameters = pathItem.parameters.filter(
1463
- (p) => !isReferenceObject(p)
1464
- );
1465
- }
1466
- let securityRequirements = void 0;
1467
- const securitySpec = operation.security ?? document.security;
1468
- if (securitySpec) {
1469
- securityRequirements = this.extractSecurityRequirements(securitySpec, document);
1470
- }
1471
- const { inputSchema, mapper } = parameterResolver.resolve(
1472
- operation,
1473
- pathParameters,
1474
- securityRequirements,
1475
- options.includeSecurityInInput
1476
- );
1477
- const responseBuilder = new ResponseBuilder(options);
1478
- const outputSchema = responseBuilder.build(operation.responses);
1479
- const name = this.generateToolName(pathStr, method, operation.operationId, options);
1480
- const description = operation.summary || operation.description || `${method.toUpperCase()} ${pathStr}`;
1481
- const metadata = this.extractMetadata(pathStr, method, operation, document, outputSchema);
1482
- const formatResolvers = {
1483
- ...options.resolveFormats ? BUILTIN_FORMAT_RESOLVERS : {},
1484
- ...options.formatResolvers
1485
- };
1486
- const hasFormatResolvers = Object.keys(formatResolvers).length > 0;
1487
- const resolvedInputSchema = hasFormatResolvers ? resolveSchemaFormats(inputSchema, formatResolvers) : inputSchema;
1488
- const resolvedOutputSchema = hasFormatResolvers && outputSchema ? resolveSchemaFormats(outputSchema, formatResolvers) : outputSchema;
1489
- return {
1490
- name,
1491
- description,
1492
- inputSchema: resolvedInputSchema,
1493
- outputSchema: resolvedOutputSchema,
1494
- mapper,
1495
- metadata
2841
+ constructor(document, rawOptions = {}) {
2842
+ this.document = document;
2843
+ const options = applySecureDefaults(rawOptions);
2844
+ this.options = {
2845
+ dereference: options.dereference ?? true,
2846
+ baseUrl: options.baseUrl ?? "",
2847
+ headers: options.headers ?? {},
2848
+ timeout: options.timeout ?? 3e4,
2849
+ validate: options.validate ?? true,
2850
+ followRedirects: options.followRedirects ?? true,
2851
+ refResolution: options.refResolution ?? {},
2852
+ secureDefaults: options.secureDefaults ?? false,
2853
+ overlays: options.overlays
1496
2854
  };
2855
+ if (this.options.overlays) {
2856
+ const overlays = Array.isArray(this.options.overlays) ? this.options.overlays : [this.options.overlays];
2857
+ for (const overlay of overlays) {
2858
+ this.document = applyOverlay(this.document, overlay);
2859
+ }
2860
+ }
1497
2861
  }
1498
2862
  /**
1499
- * Check if an operation should be included
2863
+ * Create generator from a URL
1500
2864
  */
1501
- shouldIncludeOperation(operation, path, method, options) {
1502
- if (operation.deprecated && !options.includeDeprecated) {
1503
- return false;
1504
- }
1505
- if (options.includeOperations && operation.operationId) {
1506
- if (!options.includeOperations.includes(operation.operationId)) {
1507
- return false;
2865
+ static async fromURL(url, rawOptions = {}) {
2866
+ const options = applySecureDefaults(rawOptions);
2867
+ try {
2868
+ const response = await safeFetch(url, {
2869
+ headers: options.headers,
2870
+ timeoutMs: options.timeout ?? 3e4,
2871
+ followRedirects: options.followRedirects ?? true,
2872
+ ssrf: normalizeSsrfOptions(options.refResolution)
2873
+ });
2874
+ if (!response.ok) {
2875
+ throw new LoadError(`Failed to fetch OpenAPI spec from URL: ${response.status} ${response.statusText}`, {
2876
+ url,
2877
+ status: response.status
2878
+ });
1508
2879
  }
1509
- }
1510
- if (options.excludeOperations && operation.operationId) {
1511
- if (options.excludeOperations.includes(operation.operationId)) {
1512
- return false;
2880
+ const contentType = response.headers.get("content-type") || "";
2881
+ const text = await response.text();
2882
+ let document;
2883
+ if (contentType.includes("yaml") || contentType.includes("yml") || url.match(/\.ya?ml$/i)) {
2884
+ document = yaml.parse(text);
2885
+ } else {
2886
+ document = JSON.parse(text);
1513
2887
  }
1514
- }
1515
- if (options.filterFn) {
1516
- return options.filterFn({
1517
- ...operation,
1518
- path,
1519
- method
2888
+ return new _OpenAPIToolGenerator(document, options);
2889
+ } catch (error) {
2890
+ if (error instanceof LoadError || error instanceof OverlayError) {
2891
+ throw error;
2892
+ }
2893
+ const errorMessage = error instanceof Error ? error.message : String(error);
2894
+ throw new LoadError(`Failed to load OpenAPI spec from URL: ${errorMessage}`, {
2895
+ url,
2896
+ originalError: error
1520
2897
  });
1521
2898
  }
1522
- return true;
1523
2899
  }
1524
2900
  /**
1525
- * Generate a tool name
2901
+ * Create generator from a file path
1526
2902
  */
1527
- generateToolName(path, method, operationId, options = {}) {
1528
- if (options.namingStrategy?.toolNameGenerator) {
1529
- return options.namingStrategy.toolNameGenerator(path, method, operationId);
1530
- }
1531
- if (operationId) {
1532
- return operationId;
2903
+ static async fromFile(filePath, options = {}) {
2904
+ try {
2905
+ const [path, fs] = await Promise.all([import("path"), import("fs/promises")]);
2906
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
2907
+ const content = await fs.readFile(absolutePath, "utf-8");
2908
+ const ext = path.extname(filePath).toLowerCase();
2909
+ let document;
2910
+ if (ext === ".yaml" || ext === ".yml") {
2911
+ document = yaml.parse(content);
2912
+ } else if (ext === ".json") {
2913
+ document = JSON.parse(content);
2914
+ } else {
2915
+ try {
2916
+ document = JSON.parse(content);
2917
+ } catch {
2918
+ document = yaml.parse(content);
2919
+ }
2920
+ }
2921
+ return new _OpenAPIToolGenerator(document, options);
2922
+ } catch (error) {
2923
+ if (error instanceof OverlayError) {
2924
+ throw error;
2925
+ }
2926
+ const errorMessage = error instanceof Error ? error.message : String(error);
2927
+ throw new LoadError(`Failed to load OpenAPI spec from file: ${errorMessage}`, {
2928
+ filePath,
2929
+ originalError: error
2930
+ });
1533
2931
  }
1534
- const sanitized = path.replace(/\{([^}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1535
- return `${method}_${sanitized}`;
1536
2932
  }
1537
2933
  /**
1538
- * Extract metadata from operation
2934
+ * Create generator from a YAML string
1539
2935
  */
1540
- extractMetadata(path, method, operation, document, outputSchema) {
1541
- const metadata = {
1542
- path,
1543
- method,
1544
- operationId: operation.operationId,
1545
- operationSummary: operation.summary,
1546
- operationDescription: operation.description,
1547
- tags: operation.tags,
1548
- deprecated: operation.deprecated
1549
- };
1550
- if (operation.security || document.security) {
1551
- metadata.security = this.extractSecurityRequirements(
1552
- operation.security ?? document.security,
1553
- document
1554
- );
1555
- }
1556
- const servers = operation.servers ?? document.servers;
1557
- if (servers) {
1558
- metadata.servers = servers.map((server) => ({
1559
- url: this.options.baseUrl || server.url,
1560
- description: server.description,
1561
- variables: server.variables
1562
- }));
1563
- } else if (this.options.baseUrl) {
1564
- metadata.servers = [{ url: this.options.baseUrl }];
1565
- }
1566
- const schemaObj = outputSchema;
1567
- if (schemaObj && Array.isArray(schemaObj["oneOf"])) {
1568
- const codes = schemaObj["oneOf"].map((schema) => schema["x-status-code"]).filter((code) => code !== void 0 && code !== null);
1569
- if (codes.length > 0) {
1570
- metadata.responseStatusCodes = codes;
2936
+ static async fromYAML(yamlString, options = {}) {
2937
+ try {
2938
+ const document = yaml.parse(yamlString);
2939
+ return new _OpenAPIToolGenerator(document, options);
2940
+ } catch (error) {
2941
+ if (error instanceof OverlayError) {
2942
+ throw error;
1571
2943
  }
1572
- } else if (schemaObj && schemaObj["x-status-code"] !== void 0 && schemaObj["x-status-code"] !== null) {
1573
- metadata.responseStatusCodes = [schemaObj["x-status-code"]];
1574
- }
1575
- if (operation.externalDocs) {
1576
- metadata.externalDocs = operation.externalDocs;
1577
- }
1578
- const operationWithExt = operation;
1579
- if (operationWithExt["x-frontmcp"]) {
1580
- metadata.frontmcp = operationWithExt["x-frontmcp"];
2944
+ const errorMessage = error instanceof Error ? error.message : String(error);
2945
+ throw new ParseError(`Failed to parse YAML: ${errorMessage}`, {
2946
+ originalError: error
2947
+ });
1581
2948
  }
1582
- return metadata;
1583
2949
  }
1584
2950
  /**
1585
- * Extract security requirements
2951
+ * Create generator from a JSON object
1586
2952
  */
1587
- extractSecurityRequirements(security, document) {
1588
- if (!security || !document.components?.securitySchemes) {
1589
- return [];
1590
- }
1591
- return security.flatMap(
1592
- (req) => Object.entries(req).map(([scheme, scopes]) => {
1593
- const securityScheme = document.components.securitySchemes[scheme];
1594
- if (isReferenceObject(securityScheme)) {
1595
- return { scheme, type: "http", scopes };
1596
- }
1597
- const apiKeyIn = "in" in securityScheme ? securityScheme.in : void 0;
1598
- const result = {
1599
- scheme,
1600
- type: securityScheme.type,
1601
- scopes,
1602
- name: "name" in securityScheme ? securityScheme.name : void 0,
1603
- in: apiKeyIn && (apiKeyIn === "query" || apiKeyIn === "header" || apiKeyIn === "cookie") ? apiKeyIn : void 0
1604
- };
1605
- if (securityScheme.type === "http") {
1606
- result.httpScheme = "scheme" in securityScheme ? securityScheme.scheme : void 0;
1607
- result.bearerFormat = "bearerFormat" in securityScheme ? securityScheme.bearerFormat : void 0;
1608
- }
1609
- result.description = "description" in securityScheme ? securityScheme.description : void 0;
1610
- return result;
1611
- })
1612
- );
2953
+ static async fromJSON(json, options = {}) {
2954
+ const document = JSON.parse(JSON.stringify(json));
2955
+ return new _OpenAPIToolGenerator(document, options);
1613
2956
  }
1614
- };
1615
-
1616
- // src/schema-builder.ts
1617
- var SchemaBuilder = class {
1618
2957
  /**
1619
- * Merge multiple schemas into one
2958
+ * Get the OpenAPI document
1620
2959
  */
1621
- static merge(schemas) {
1622
- if (schemas.length === 0) {
1623
- return { type: "object" };
1624
- }
1625
- if (schemas.length === 1) {
1626
- return schemas[0];
1627
- }
1628
- const merged = {
1629
- type: "object",
1630
- properties: {},
1631
- required: []
1632
- };
1633
- const allRequired = /* @__PURE__ */ new Set();
1634
- for (const schema of schemas) {
1635
- if (schema.properties) {
1636
- merged.properties = {
1637
- ...merged.properties,
1638
- ...schema.properties
1639
- };
1640
- }
1641
- if (schema.required) {
1642
- schema.required.forEach((field) => allRequired.add(field));
1643
- }
1644
- }
1645
- if (allRequired.size > 0) {
1646
- merged.required = Array.from(allRequired);
1647
- }
1648
- return merged;
2960
+ getDocument() {
2961
+ return this.dereferencedDocument ?? this.document;
1649
2962
  }
1650
2963
  /**
1651
- * Create a union schema (oneOf)
2964
+ * Validate the OpenAPI document
1652
2965
  */
1653
- static union(schemas) {
1654
- if (schemas.length === 0) {
1655
- return {};
1656
- }
1657
- if (schemas.length === 1) {
1658
- return schemas[0];
1659
- }
1660
- return {
1661
- oneOf: schemas
1662
- };
2966
+ async validate() {
2967
+ const validator = new Validator();
2968
+ return validator.validate(this.document);
1663
2969
  }
1664
2970
  /**
1665
- * Deep clone a schema
2971
+ * Lint the loaded document for agent-readiness (missing operationIds,
2972
+ * vague descriptions, unpaginated lists, oversized schemas, ...). Runs
2973
+ * after overlays and dereferencing so findings reflect what tools would
2974
+ * actually be generated from.
1666
2975
  */
1667
- static clone(schema) {
1668
- return JSON.parse(JSON.stringify(schema));
2976
+ async lint() {
2977
+ await this.initialize(false);
2978
+ return lintDocument(this.getDocument());
1669
2979
  }
2980
+ // NOTE: internal/private-address blocking + IPv4-mapped-IPv6 decoding now live
2981
+ // in `ssrf.ts` (`isBlockedHostname` / `isBlockedAddress` / `decodeIpv4MappedIpv6`),
2982
+ // shared by the spec-URL fetch (`fromURL`) and the `$ref` resolver below, and
2983
+ // augmented there with DNS resolution (closing the DNS-name-to-internal bypass)
2984
+ // and per-hop redirect re-validation (`safeFetch`).
1670
2985
  /**
1671
- * Remove $ref from schema (assumes already dereferenced)
2986
+ * Build $RefParser options based on refResolution configuration.
2987
+ * Defaults: allow http/https, block file://, block internal IPs.
1672
2988
  */
1673
- static removeRefs(schema) {
1674
- const cloned = this.clone(schema);
1675
- this.removeRefsRecursive(cloned);
1676
- return cloned;
1677
- }
1678
- static removeRefsRecursive(obj) {
1679
- if (!obj || typeof obj !== "object") return;
1680
- if (obj.$ref) {
1681
- delete obj.$ref;
2989
+ buildRefParserOptions() {
2990
+ const raw = this.options.refResolution;
2991
+ const refOpts = {
2992
+ allowedProtocols: raw.allowedProtocols ?? ["http", "https"],
2993
+ allowedHosts: raw.allowedHosts ?? [],
2994
+ blockedHosts: raw.blockedHosts ?? [],
2995
+ allowInternalIPs: raw.allowInternalIPs ?? false
2996
+ };
2997
+ const allowedProtocols = new Set(refOpts.allowedProtocols);
2998
+ const hasNetworkProtocol = allowedProtocols.size > 0 && !([...allowedProtocols].length === 1 && allowedProtocols.has("file"));
2999
+ if (allowedProtocols.size === 0) {
3000
+ return { resolve: { external: false } };
1682
3001
  }
1683
- for (const key in obj) {
1684
- if (key in obj) {
1685
- const value = obj[key];
1686
- if (value && typeof value === "object") {
1687
- this.removeRefsRecursive(value);
3002
+ const resolveConfig = {
3003
+ external: true,
3004
+ file: allowedProtocols.has("file") ? void 0 : false
3005
+ };
3006
+ if (hasNetworkProtocol) {
3007
+ const hasHostAllowlist = refOpts.allowedHosts.length > 0;
3008
+ const hostAllowSet = new Set(refOpts.allowedHosts);
3009
+ resolveConfig["http"] = {
3010
+ // SECURITY: never auto-follow HTTP redirects when resolving external
3011
+ // `$ref`s. `canRead` validates only the INITIAL URL; the resolver's
3012
+ // default redirect-following (up to 5 hops) re-fetches the `Location`
3013
+ // target WITHOUT re-invoking `canRead`, so an allowlisted host could
3014
+ // 302 → `http://169.254.169.254/...` and smuggle a blocked target past
3015
+ // the allow/deny lists. `redirects: 0` refuses the first redirect, and
3016
+ // our custom `read` (below) additionally refuses redirects itself.
3017
+ redirects: 0,
3018
+ // Synchronous gate: protocol, host allow-list, and literal/known
3019
+ // internal hosts. DNS names that *resolve* to internal addresses pass
3020
+ // here (canRead cannot be async) and are caught in `read` via DNS
3021
+ // resolution — closing the `127.0.0.1.nip.io` bypass for `$ref`s too.
3022
+ canRead: (file) => {
3023
+ try {
3024
+ const parsed = new URL(file.url);
3025
+ const protocol = parsed.protocol.replace(":", "");
3026
+ if (!allowedProtocols.has(protocol)) {
3027
+ return false;
3028
+ }
3029
+ if (hasHostAllowlist && !hostAllowSet.has(parsed.hostname)) {
3030
+ return false;
3031
+ }
3032
+ if (isBlockedHostname(parsed.hostname, refOpts)) {
3033
+ return false;
3034
+ }
3035
+ return true;
3036
+ } catch {
3037
+ return false;
3038
+ }
3039
+ },
3040
+ // SSRF-safe fetch: resolves DNS and rejects names that map to internal
3041
+ // addresses, and refuses redirects. NOTE: deliberately does NOT forward
3042
+ // `this.options.headers` (the spec-load credentials) to third-party
3043
+ // `$ref` hosts — that would leak the spec's auth token cross-origin.
3044
+ read: async (file) => {
3045
+ const response = await safeFetch(file.url, {
3046
+ timeoutMs: this.options.timeout,
3047
+ followRedirects: false,
3048
+ ssrf: refOpts
3049
+ });
3050
+ if (!response.ok) {
3051
+ throw new LoadError(
3052
+ `Failed to resolve external $ref "${file.url}": ${response.status} ${response.statusText}`,
3053
+ { url: file.url, status: response.status }
3054
+ );
3055
+ }
3056
+ return response.text();
1688
3057
  }
1689
- }
3058
+ };
3059
+ } else {
3060
+ resolveConfig["http"] = false;
1690
3061
  }
3062
+ return { resolve: resolveConfig };
1691
3063
  }
1692
3064
  /**
1693
- * Add description to schema
1694
- */
1695
- static withDescription(schema, description) {
1696
- return {
1697
- ...schema,
1698
- description
1699
- };
1700
- }
1701
- /**
1702
- * Add example to schema
1703
- */
1704
- static withExample(schema, example) {
1705
- const existingExamples = Array.isArray(schema.examples) ? schema.examples : [];
1706
- return {
1707
- ...schema,
1708
- examples: [...existingExamples, example]
1709
- };
1710
- }
1711
- /**
1712
- * Add default value to schema
1713
- */
1714
- static withDefault(schema, defaultValue) {
1715
- return {
1716
- ...schema,
1717
- default: defaultValue
1718
- };
1719
- }
1720
- /**
1721
- * Add format to schema
3065
+ * Does the document contain any EXTERNAL `$ref` (a ref that is not a local
3066
+ * JSON-pointer beginning with `#`)? Only external refs require the full
3067
+ * `$RefParser` (file/http resolvers, which pull Node builtins). A document
3068
+ * with only internal refs can be dereferenced with the runtime-agnostic
3069
+ * resolver below — so it works on V8 isolates (Cloudflare Workers) too.
1722
3070
  */
1723
- static withFormat(schema, format) {
1724
- return {
1725
- ...schema,
1726
- format
1727
- };
3071
+ static hasExternalRefs(node, seen = /* @__PURE__ */ new Set()) {
3072
+ if (node === null || typeof node !== "object") return false;
3073
+ if (seen.has(node)) return false;
3074
+ seen.add(node);
3075
+ if (Array.isArray(node)) return node.some((n) => _OpenAPIToolGenerator.hasExternalRefs(n, seen));
3076
+ const ref = node.$ref;
3077
+ if (typeof ref === "string" && !ref.startsWith("#")) return true;
3078
+ return Object.values(node).some(
3079
+ (v) => _OpenAPIToolGenerator.hasExternalRefs(v, seen)
3080
+ );
1728
3081
  }
1729
3082
  /**
1730
- * Add pattern to schema
3083
+ * Dereference local (`#/...`) `$ref`s without `$RefParser` — pure, dependency-
3084
+ * free, runtime-agnostic. A pointer cache makes circular schemas resolve to a
3085
+ * shared reference instead of recursing forever (same contract as `$RefParser`).
1731
3086
  */
1732
- static withPattern(schema, pattern) {
1733
- return {
1734
- ...schema,
1735
- pattern
3087
+ static dereferenceInternal(root) {
3088
+ const cache = /* @__PURE__ */ new Map();
3089
+ const resolvePointer = (ptr) => {
3090
+ const parts = ptr.replace(/^#\/?/, "").split("/").filter((p) => p.length > 0).map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
3091
+ let cur = root;
3092
+ for (const p of parts) cur = cur?.[p];
3093
+ return cur;
1736
3094
  };
1737
- }
1738
- /**
1739
- * Add enum to schema
1740
- */
1741
- static withEnum(schema, values) {
1742
- return {
1743
- ...schema,
1744
- enum: values
3095
+ const walk = (node) => {
3096
+ if (node === null || typeof node !== "object") return node;
3097
+ if (Array.isArray(node)) return node.map(walk);
3098
+ const ref = node.$ref;
3099
+ if (typeof ref === "string" && ref.startsWith("#")) {
3100
+ const cached = cache.get(ref);
3101
+ if (cached !== void 0) return cached;
3102
+ const placeholder = {};
3103
+ cache.set(ref, placeholder);
3104
+ const resolved = walk(resolvePointer(ref));
3105
+ if (resolved && typeof resolved === "object") Object.assign(placeholder, resolved);
3106
+ return placeholder;
3107
+ }
3108
+ const out = {};
3109
+ for (const [k, v] of Object.entries(node)) out[k] = walk(v);
3110
+ return out;
1745
3111
  };
3112
+ return walk(root);
1746
3113
  }
1747
3114
  /**
1748
- * Add minimum/maximum constraints
3115
+ * Initialize the generator (dereference if needed, then validate)
1749
3116
  */
1750
- static withRange(schema, min, max, options = {}) {
1751
- const result = { ...schema };
1752
- if (min !== void 0) {
1753
- if (options.exclusive) {
1754
- result.exclusiveMinimum = min;
3117
+ async initialize(runValidation = this.options.validate) {
3118
+ if (this.options.dereference && !this.dereferencedDocument) {
3119
+ const cloned = JSON.parse(JSON.stringify(this.document));
3120
+ if (!_OpenAPIToolGenerator.hasExternalRefs(cloned)) {
3121
+ this.dereferencedDocument = _OpenAPIToolGenerator.dereferenceInternal(cloned);
1755
3122
  } else {
1756
- result.minimum = min;
3123
+ try {
3124
+ const { default: $RefParser } = await import("@apidevtools/json-schema-ref-parser");
3125
+ const refParserOptions = this.buildRefParserOptions();
3126
+ this.dereferencedDocument = await $RefParser.dereference(cloned, refParserOptions);
3127
+ } catch (error) {
3128
+ const errorMessage = error instanceof Error ? error.message : String(error);
3129
+ throw new ParseError(`Failed to dereference OpenAPI document: ${errorMessage}`, {
3130
+ originalError: error
3131
+ });
3132
+ }
1757
3133
  }
1758
3134
  }
1759
- if (max !== void 0) {
1760
- if (options.exclusive) {
1761
- result.exclusiveMaximum = max;
1762
- } else {
1763
- result.maximum = max;
3135
+ if (runValidation) {
3136
+ const validator = new Validator();
3137
+ const documentToValidate = this.dereferencedDocument ?? this.document;
3138
+ const result = await validator.validate(documentToValidate);
3139
+ if (!result.valid) {
3140
+ throw new ParseError("Invalid OpenAPI document", { errors: result.errors });
1764
3141
  }
1765
3142
  }
1766
- return result;
1767
3143
  }
1768
3144
  /**
1769
- * Add minLength/maxLength constraints
3145
+ * Generate all tools from the OpenAPI specification
1770
3146
  */
1771
- static withLength(schema, minLength, maxLength) {
1772
- const result = { ...schema };
1773
- if (minLength !== void 0) {
1774
- result.minLength = minLength;
3147
+ async generateTools(options = {}) {
3148
+ await this.initialize();
3149
+ const document = this.getDocument();
3150
+ const tools = [];
3151
+ const usedNames = /* @__PURE__ */ new Set();
3152
+ if (!document.paths) {
3153
+ return tools;
1775
3154
  }
1776
- if (maxLength !== void 0) {
1777
- result.maxLength = maxLength;
3155
+ const sortedPaths = Object.entries(document.paths).sort(([a], [b]) => a < b ? -1 : 1);
3156
+ for (const [pathStr, pathItem] of sortedPaths) {
3157
+ if (!pathItem || "$ref" in pathItem) continue;
3158
+ const methods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
3159
+ for (const method of methods) {
3160
+ const operation = pathItem[method];
3161
+ if (!operation) continue;
3162
+ if (!this.shouldIncludeOperation(operation, pathStr, method, options, document, pathItem)) {
3163
+ continue;
3164
+ }
3165
+ try {
3166
+ let tool = await this.generateTool(pathStr, method, options);
3167
+ if (usedNames.has(tool.name)) {
3168
+ const maxLength = options.maxToolNameLength ?? DEFAULT_MAX_TOOL_NAME_LENGTH;
3169
+ let seed = `${method} ${pathStr}`;
3170
+ let deduped = normalizeToolName(`${tool.name}_${fnv1aHex(seed)}`, maxLength, seed);
3171
+ let attempts = 1;
3172
+ while (usedNames.has(deduped)) {
3173
+ if (attempts >= MAX_NAME_DEDUP_ATTEMPTS) {
3174
+ throw new GenerationError(
3175
+ `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.`,
3176
+ { name: tool.name, method, path: pathStr, maxToolNameLength: maxLength }
3177
+ );
3178
+ }
3179
+ seed += "#";
3180
+ deduped = normalizeToolName(`${tool.name}_${fnv1aHex(seed)}`, maxLength, seed);
3181
+ attempts++;
3182
+ }
3183
+ tool = { ...tool, name: deduped };
3184
+ }
3185
+ usedNames.add(tool.name);
3186
+ tools.push(tool);
3187
+ } catch (error) {
3188
+ const errorMessage = error instanceof Error ? error.message : String(error);
3189
+ console.warn(`Failed to generate tool for ${method.toUpperCase()} ${pathStr}:`, errorMessage);
3190
+ }
3191
+ }
1778
3192
  }
1779
- return result;
1780
- }
1781
- /**
1782
- * Create object schema
1783
- */
1784
- static object(properties, required) {
1785
- return {
1786
- type: "object",
1787
- properties,
1788
- ...required && required.length > 0 && { required },
1789
- additionalProperties: false
1790
- };
1791
- }
1792
- /**
1793
- * Create array schema
1794
- */
1795
- static array(items, constraints) {
1796
- return {
1797
- type: "array",
1798
- items,
1799
- ...constraints
1800
- };
3193
+ return tools;
1801
3194
  }
1802
3195
  /**
1803
- * Create string schema
3196
+ * Generate a specific tool for a path and method
1804
3197
  */
1805
- static string(constraints) {
1806
- return {
1807
- type: "string",
1808
- ...constraints
3198
+ async generateTool(pathStr, method, options = {}) {
3199
+ await this.initialize();
3200
+ const document = this.getDocument();
3201
+ if (!document.paths) {
3202
+ throw new Error("No paths defined in OpenAPI document");
3203
+ }
3204
+ const pathItem = document.paths[pathStr];
3205
+ const operation = pathItem?.[method.toLowerCase()];
3206
+ if (!operation) {
3207
+ throw new Error(`Operation not found: ${method.toUpperCase()} ${pathStr}`);
3208
+ }
3209
+ const parameterResolver = new ParameterResolver(options.namingStrategy, {
3210
+ includeExamples: options.includeExamples
3211
+ });
3212
+ let pathParameters = void 0;
3213
+ if (pathItem.parameters) {
3214
+ pathParameters = pathItem.parameters.filter(
3215
+ (p) => !isReferenceObject(p)
3216
+ );
3217
+ }
3218
+ let securityRequirements = void 0;
3219
+ const securitySpec = operation.security ?? document.security;
3220
+ if (securitySpec) {
3221
+ securityRequirements = this.extractSecurityRequirements(securitySpec, document);
3222
+ }
3223
+ const { inputSchema, mapper } = parameterResolver.resolve(
3224
+ operation,
3225
+ pathParameters,
3226
+ securityRequirements,
3227
+ options.includeSecurityInInput
3228
+ );
3229
+ const responseBuilder = new ResponseBuilder(options);
3230
+ const outputSchema = responseBuilder.build(operation.responses);
3231
+ const overrides = extractExtensionOverrides(operation);
3232
+ const name = this.generateToolName(pathStr, method, overrides.name ?? operation.operationId, options);
3233
+ const description = overrides.description ?? composeDescription(operation, method, pathStr, options.descriptionStrategy ?? "summaryOnly");
3234
+ const title = overrides.title ?? operation.summary;
3235
+ const inferred = options.inferAnnotations !== false ? inferAnnotationsFromMethod(method.toLowerCase()) : void 0;
3236
+ const annotations = inferred || overrides.annotations ? { ...inferred, ...overrides.annotations } : void 0;
3237
+ const metadata = this.extractMetadata(pathStr, method, operation, document, outputSchema);
3238
+ const formatResolvers = {
3239
+ ...options.resolveFormats ? BUILTIN_FORMAT_RESOLVERS : {},
3240
+ ...options.formatResolvers
1809
3241
  };
1810
- }
1811
- /**
1812
- * Create number schema
1813
- */
1814
- static number(constraints) {
1815
- return {
1816
- type: "number",
1817
- ...constraints
3242
+ const hasFormatResolvers = Object.keys(formatResolvers).length > 0;
3243
+ let resolvedInputSchema = hasFormatResolvers ? resolveSchemaFormats(inputSchema, formatResolvers) : inputSchema;
3244
+ let resolvedOutputSchema = hasFormatResolvers && outputSchema ? resolveSchemaFormats(outputSchema, formatResolvers) : outputSchema;
3245
+ const maxSchemaDepth = Math.max(1, options.maxSchemaDepth ?? 10);
3246
+ resolvedInputSchema = SchemaBuilder.truncateDepth(resolvedInputSchema, maxSchemaDepth);
3247
+ if (resolvedOutputSchema) {
3248
+ resolvedOutputSchema = SchemaBuilder.truncateDepth(resolvedOutputSchema, maxSchemaDepth);
3249
+ }
3250
+ const applyTrim = (schema, isInputRoot) => {
3251
+ let trimmed = schema;
3252
+ if (options.stripExamples) trimmed = SchemaBuilder.stripExamples(trimmed);
3253
+ if (options.maxDescriptionLength !== void 0) {
3254
+ trimmed = SchemaBuilder.capDescriptions(trimmed, options.maxDescriptionLength);
3255
+ }
3256
+ if (options.maxProperties !== void 0) {
3257
+ if (isInputRoot) {
3258
+ const properties = trimmed.properties;
3259
+ if (properties && typeof properties === "object") {
3260
+ const limited = {};
3261
+ for (const [key, value] of Object.entries(properties)) {
3262
+ limited[key] = SchemaBuilder.limitProperties(value, options.maxProperties);
3263
+ }
3264
+ trimmed = { ...trimmed, properties: limited };
3265
+ }
3266
+ } else {
3267
+ trimmed = SchemaBuilder.limitProperties(trimmed, options.maxProperties);
3268
+ }
3269
+ }
3270
+ return trimmed;
1818
3271
  };
1819
- }
1820
- /**
1821
- * Create integer schema
1822
- */
1823
- static integer(constraints) {
3272
+ if (options.stripExamples || options.maxProperties !== void 0 || options.maxDescriptionLength !== void 0) {
3273
+ resolvedInputSchema = applyTrim(resolvedInputSchema, true);
3274
+ if (resolvedOutputSchema) {
3275
+ resolvedOutputSchema = applyTrim(resolvedOutputSchema, false);
3276
+ }
3277
+ }
3278
+ if (options.target) {
3279
+ resolvedInputSchema = applyClientTarget(resolvedInputSchema, options.target);
3280
+ if (resolvedOutputSchema) {
3281
+ resolvedOutputSchema = applyClientTarget(resolvedOutputSchema, options.target);
3282
+ }
3283
+ }
3284
+ const responseHints = detectResponseHints(resolvedOutputSchema, mapper);
3285
+ if (responseHints) {
3286
+ metadata.responseHints = responseHints;
3287
+ }
3288
+ let finalDescription = description;
3289
+ if (options.appendResponseSummary && resolvedOutputSchema) {
3290
+ const summary = summarizeOutputSchema(resolvedOutputSchema);
3291
+ if (summary) {
3292
+ finalDescription = `${finalDescription}
3293
+
3294
+ Returns: ${summary}`;
3295
+ }
3296
+ }
1824
3297
  return {
1825
- type: "integer",
1826
- ...constraints
3298
+ name,
3299
+ ...title !== void 0 && { title },
3300
+ description: finalDescription,
3301
+ ...annotations && { annotations },
3302
+ inputSchema: resolvedInputSchema,
3303
+ outputSchema: resolvedOutputSchema,
3304
+ mapper,
3305
+ metadata
1827
3306
  };
1828
3307
  }
1829
3308
  /**
1830
- * Create boolean schema
3309
+ * Check if an operation should be included
1831
3310
  */
1832
- static boolean() {
1833
- return {
1834
- type: "boolean"
1835
- };
3311
+ shouldIncludeOperation(operation, path, method, options, document, pathItem) {
3312
+ if (!resolveExtensionEnabled(document, pathItem, operation)) {
3313
+ return false;
3314
+ }
3315
+ if (operation.deprecated && !options.includeDeprecated) {
3316
+ return false;
3317
+ }
3318
+ const lowerMethod = method.toLowerCase();
3319
+ if (options.includeMethods && !options.includeMethods.includes(lowerMethod)) {
3320
+ return false;
3321
+ }
3322
+ if (options.excludeMethods?.includes(lowerMethod)) {
3323
+ return false;
3324
+ }
3325
+ if (options.includePaths && !matchesAnyGlob(path, options.includePaths)) {
3326
+ return false;
3327
+ }
3328
+ if (options.excludePaths && matchesAnyGlob(path, options.excludePaths)) {
3329
+ return false;
3330
+ }
3331
+ const tags = operation.tags ?? [];
3332
+ if (options.includeTags && !tags.some((tag) => options.includeTags.includes(tag))) {
3333
+ return false;
3334
+ }
3335
+ if (options.excludeTags && tags.some((tag) => options.excludeTags.includes(tag))) {
3336
+ return false;
3337
+ }
3338
+ if (options.includeOperations && operation.operationId) {
3339
+ if (!options.includeOperations.includes(operation.operationId)) {
3340
+ return false;
3341
+ }
3342
+ }
3343
+ if (options.excludeOperations && operation.operationId) {
3344
+ if (options.excludeOperations.includes(operation.operationId)) {
3345
+ return false;
3346
+ }
3347
+ }
3348
+ if (options.readOnlyOnly) {
3349
+ const effective = {
3350
+ ...inferAnnotationsFromMethod(lowerMethod),
3351
+ ...extractExtensionOverrides(operation).annotations
3352
+ };
3353
+ if (effective.readOnlyHint !== true) {
3354
+ return false;
3355
+ }
3356
+ }
3357
+ if (options.filterFn) {
3358
+ return options.filterFn({
3359
+ ...operation,
3360
+ path,
3361
+ method
3362
+ });
3363
+ }
3364
+ return true;
1836
3365
  }
1837
3366
  /**
1838
- * Create null schema
3367
+ * Generate a tool name
1839
3368
  */
1840
- static null() {
1841
- return {
1842
- type: "null"
1843
- };
3369
+ generateToolName(path, method, operationId, options = {}) {
3370
+ let rawName;
3371
+ if (options.namingStrategy?.toolNameGenerator) {
3372
+ rawName = options.namingStrategy.toolNameGenerator(path, method, operationId);
3373
+ } else if (operationId) {
3374
+ rawName = operationId;
3375
+ } else {
3376
+ const sanitized = trimUnderscores(
3377
+ path.replace(/\{([^{}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_")
3378
+ );
3379
+ rawName = `${method}_${sanitized}`;
3380
+ }
3381
+ return normalizeToolName(
3382
+ rawName,
3383
+ options.maxToolNameLength ?? DEFAULT_MAX_TOOL_NAME_LENGTH,
3384
+ `${method} ${path}`
3385
+ );
1844
3386
  }
1845
3387
  /**
1846
- * Flatten nested oneOf/anyOf/allOf schemas
3388
+ * Extract metadata from operation
1847
3389
  */
1848
- static flatten(schema, maxDepth = 10) {
1849
- if (maxDepth <= 0) return schema;
1850
- const cloned = this.clone(schema);
1851
- if (cloned.oneOf) {
1852
- const flattened = cloned.oneOf.flatMap((s) => {
1853
- const sub = this.flatten(s, maxDepth - 1);
1854
- return sub.oneOf ? sub.oneOf : [sub];
1855
- });
1856
- cloned.oneOf = flattened;
3390
+ extractMetadata(path, method, operation, document, outputSchema) {
3391
+ const metadata = {
3392
+ path,
3393
+ method,
3394
+ operationId: operation.operationId,
3395
+ operationSummary: operation.summary,
3396
+ operationDescription: operation.description,
3397
+ tags: operation.tags,
3398
+ deprecated: operation.deprecated
3399
+ };
3400
+ if (operation.security || document.security) {
3401
+ metadata.security = this.extractSecurityRequirements(
3402
+ operation.security ?? document.security,
3403
+ document
3404
+ );
1857
3405
  }
1858
- if (cloned.anyOf) {
1859
- const flattened = cloned.anyOf.flatMap((s) => {
1860
- const sub = this.flatten(s, maxDepth - 1);
1861
- return sub.anyOf ? sub.anyOf : [sub];
1862
- });
1863
- cloned.anyOf = flattened;
3406
+ const servers = operation.servers ?? document.servers;
3407
+ if (servers) {
3408
+ metadata.servers = servers.map((server) => ({
3409
+ url: this.options.baseUrl || server.url,
3410
+ description: server.description,
3411
+ variables: server.variables
3412
+ }));
3413
+ } else if (this.options.baseUrl) {
3414
+ metadata.servers = [{ url: this.options.baseUrl }];
1864
3415
  }
1865
- if (cloned.allOf) {
1866
- const flattened = cloned.allOf.flatMap((s) => {
1867
- const sub = this.flatten(s, maxDepth - 1);
1868
- return sub.allOf ? sub.allOf : [sub];
1869
- });
1870
- cloned.allOf = flattened;
3416
+ const schemaObj = outputSchema;
3417
+ if (schemaObj && Array.isArray(schemaObj["oneOf"])) {
3418
+ const codes = schemaObj["oneOf"].map((schema) => schema["x-status-code"]).filter((code) => code !== void 0 && code !== null);
3419
+ if (codes.length > 0) {
3420
+ metadata.responseStatusCodes = codes;
3421
+ }
3422
+ } else if (schemaObj && schemaObj["x-status-code"] !== void 0 && schemaObj["x-status-code"] !== null) {
3423
+ metadata.responseStatusCodes = [schemaObj["x-status-code"]];
1871
3424
  }
1872
- return cloned;
3425
+ if (operation.externalDocs) {
3426
+ metadata.externalDocs = operation.externalDocs;
3427
+ }
3428
+ const operationWithExt = operation;
3429
+ if (operationWithExt["x-frontmcp"]) {
3430
+ metadata.frontmcp = operationWithExt["x-frontmcp"];
3431
+ }
3432
+ return metadata;
1873
3433
  }
1874
3434
  /**
1875
- * Simplify schema by removing unnecessary fields
3435
+ * Extract security requirements
1876
3436
  */
1877
- static simplify(schema) {
1878
- const cloned = this.clone(schema);
1879
- if (Array.isArray(cloned.required) && cloned.required.length === 0) {
1880
- delete cloned.required;
1881
- }
1882
- if (cloned.properties && Object.keys(cloned.properties).length === 0) {
1883
- delete cloned.properties;
1884
- }
1885
- if (Array.isArray(cloned.examples) && cloned.examples.length === 0) {
1886
- delete cloned.examples;
1887
- }
1888
- if (cloned.title && cloned.description && cloned.title === cloned.description) {
1889
- delete cloned.title;
3437
+ extractSecurityRequirements(security, document) {
3438
+ if (!security || !document.components?.securitySchemes) {
3439
+ return [];
1890
3440
  }
1891
- return cloned;
3441
+ return security.flatMap(
3442
+ (req) => Object.entries(req).map(([scheme, scopes]) => {
3443
+ const securityScheme = document.components.securitySchemes[scheme];
3444
+ if (isReferenceObject(securityScheme)) {
3445
+ return { scheme, type: "http", scopes };
3446
+ }
3447
+ const apiKeyIn = "in" in securityScheme ? securityScheme.in : void 0;
3448
+ const result = {
3449
+ scheme,
3450
+ type: securityScheme.type,
3451
+ scopes,
3452
+ name: "name" in securityScheme ? securityScheme.name : void 0,
3453
+ in: apiKeyIn && (apiKeyIn === "query" || apiKeyIn === "header" || apiKeyIn === "cookie") ? apiKeyIn : void 0
3454
+ };
3455
+ if (securityScheme.type === "http") {
3456
+ result.httpScheme = "scheme" in securityScheme ? securityScheme.scheme : void 0;
3457
+ result.bearerFormat = "bearerFormat" in securityScheme ? securityScheme.bearerFormat : void 0;
3458
+ }
3459
+ result.description = "description" in securityScheme ? securityScheme.description : void 0;
3460
+ return result;
3461
+ })
3462
+ );
1892
3463
  }
1893
3464
  };
1894
3465
 
@@ -2019,7 +3590,7 @@ var SecurityResolver = class {
2019
3590
  resolveDigestAuth(context) {
2020
3591
  const digest = context.digest;
2021
3592
  if (!digest) return void 0;
2022
- const quoted = (v) => String(v).replace(/[\r\n]/g, "").replace(/"/g, '\\"');
3593
+ const quoted = (v) => String(v).replace(/[\r\n]/g, "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2023
3594
  const token = (v) => String(v).replace(/[\r\n",]/g, "");
2024
3595
  const parts = [
2025
3596
  `username="${quoted(digest.username)}"`,
@@ -2135,6 +3706,389 @@ function createSecurityContext(auth) {
2135
3706
  customResolver: auth.customResolver
2136
3707
  };
2137
3708
  }
3709
+
3710
+ // src/request-builder.ts
3711
+ var RESERVED_DECODE = {
3712
+ "%3A": ":",
3713
+ "%2F": "/",
3714
+ "%3F": "?",
3715
+ "%23": "#",
3716
+ "%5B": "[",
3717
+ "%5D": "]",
3718
+ "%40": "@",
3719
+ "%24": "$",
3720
+ "%26": "&",
3721
+ "%2B": "+",
3722
+ "%2C": ",",
3723
+ "%3B": ";",
3724
+ "%3D": "="
3725
+ };
3726
+ function encodeValue(value, allowReserved) {
3727
+ const encoded = encodeURIComponent(value);
3728
+ if (!allowReserved) return encoded;
3729
+ return encoded.replace(/%3A|%2F|%3F|%23|%5B|%5D|%40|%24|%26|%2B|%2C|%3B|%3D/gi, (m) => RESERVED_DECODE[m.toUpperCase()]);
3730
+ }
3731
+ function isPlainObject(value) {
3732
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3733
+ }
3734
+ function primitiveString(value, paramName, location) {
3735
+ if (value === null || value === void 0 || typeof value === "object") {
3736
+ throw new RequestBuildError(
3737
+ `${location} parameter '${paramName}' must serialize to a primitive; received ${value === null ? "null" : Array.isArray(value) ? "an array" : typeof value}`,
3738
+ { param: paramName, location }
3739
+ );
3740
+ }
3741
+ return String(value);
3742
+ }
3743
+ function serializePathValue(mapper, value) {
3744
+ const style = mapper.style ?? "simple";
3745
+ const explode = mapper.explode ?? false;
3746
+ const name = mapper.key;
3747
+ const enc = (v) => encodeValue(primitiveString(v, name, "path"));
3748
+ if (Array.isArray(value)) {
3749
+ if (style === "label") {
3750
+ return `.${value.map(enc).join(explode ? "." : ",")}`;
3751
+ }
3752
+ if (style === "matrix") {
3753
+ return explode ? value.map((v) => `;${name}=${enc(v)}`).join("") : `;${name}=${value.map(enc).join(",")}`;
3754
+ }
3755
+ return value.map(enc).join(",");
3756
+ }
3757
+ if (isPlainObject(value)) {
3758
+ const entries = Object.entries(value);
3759
+ if (style === "label") {
3760
+ return explode ? entries.map(([k, v]) => `.${encodeValue(k)}=${enc(v)}`).join("") : `.${entries.map(([k, v]) => `${encodeValue(k)},${enc(v)}`).join(",")}`;
3761
+ }
3762
+ if (style === "matrix") {
3763
+ return explode ? entries.map(([k, v]) => `;${encodeValue(k)}=${enc(v)}`).join("") : `;${name}=${entries.map(([k, v]) => `${encodeValue(k)},${enc(v)}`).join(",")}`;
3764
+ }
3765
+ return explode ? entries.map(([k, v]) => `${encodeValue(k)}=${enc(v)}`).join(",") : entries.map(([k, v]) => `${encodeValue(k)},${enc(v)}`).join(",");
3766
+ }
3767
+ const core = enc(value);
3768
+ if (style === "label") return `.${core}`;
3769
+ if (style === "matrix") return `;${name}=${core}`;
3770
+ return core;
3771
+ }
3772
+ function serializeQueryPairs(mapper, value) {
3773
+ const style = mapper.style ?? "form";
3774
+ const explode = mapper.explode ?? style === "form";
3775
+ const name = mapper.key;
3776
+ const str = (v) => primitiveString(v, name, "query");
3777
+ if (Array.isArray(value)) {
3778
+ if ((style === "deepObject" ? mapper.explode ?? true : explode) || value.length === 0) {
3779
+ return value.map((v) => [name, str(v)]);
3780
+ }
3781
+ const delimiter = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ",";
3782
+ return [[name, value.map(str).join(delimiter)]];
3783
+ }
3784
+ if (isPlainObject(value)) {
3785
+ if (style === "deepObject") {
3786
+ const pairs = [];
3787
+ const walk = (prefix, node) => {
3788
+ for (const [k, v] of Object.entries(node)) {
3789
+ if (v === void 0) continue;
3790
+ if (isPlainObject(v)) {
3791
+ walk(`${prefix}[${k}]`, v);
3792
+ } else if (Array.isArray(v)) {
3793
+ for (const item of v) pairs.push([`${prefix}[${k}]`, str(item)]);
3794
+ } else {
3795
+ pairs.push([`${prefix}[${k}]`, str(v)]);
3796
+ }
3797
+ }
3798
+ };
3799
+ walk(name, value);
3800
+ return pairs;
3801
+ }
3802
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0);
3803
+ if (explode) {
3804
+ return entries.map(([k, v]) => [k, str(v)]);
3805
+ }
3806
+ return [[name, entries.map(([k, v]) => `${k},${str(v)}`).join(",")]];
3807
+ }
3808
+ return [[name, str(value)]];
3809
+ }
3810
+ function serializeHeaderValue(mapper, value) {
3811
+ const explode = mapper.explode ?? false;
3812
+ const name = mapper.key;
3813
+ const str = (v) => primitiveString(v, name, "header");
3814
+ if (Array.isArray(value)) {
3815
+ return value.map(str).join(",");
3816
+ }
3817
+ if (isPlainObject(value)) {
3818
+ const entries = Object.entries(value);
3819
+ return explode ? entries.map(([k, v]) => `${k}=${str(v)}`).join(",") : entries.map(([k, v]) => `${k},${str(v)}`).join(",");
3820
+ }
3821
+ return str(value);
3822
+ }
3823
+ function assertHeaderSafe(name, value) {
3824
+ if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
3825
+ throw new RequestBuildError(`Invalid header name '${name}' (RFC 7230 token required)`, { header: name });
3826
+ }
3827
+ if (/[\r\n\x00]/.test(value)) {
3828
+ throw new RequestBuildError(`Header '${name}' value contains control characters (possible header injection)`, {
3829
+ header: name
3830
+ });
3831
+ }
3832
+ }
3833
+ function assertCookieName(name) {
3834
+ if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
3835
+ throw new RequestBuildError(`Invalid cookie name '${name}' (RFC 6265 token required)`, { cookie: name });
3836
+ }
3837
+ }
3838
+ function assertCookieValue(name, value) {
3839
+ if (/[\x00-\x1f\x7f\s";\\]/.test(value)) {
3840
+ throw new RequestBuildError(
3841
+ `Cookie '${name}' value contains characters that break the Cookie header (RFC 6265 cookie-octet violation)`,
3842
+ { cookie: name }
3843
+ );
3844
+ }
3845
+ }
3846
+ function formatSecurityValue(mapper, value) {
3847
+ const security = mapper.security;
3848
+ if (security.type === "http") {
3849
+ const scheme = (security.httpScheme ?? "bearer").toLowerCase();
3850
+ if (scheme !== "bearer" && scheme !== "basic") {
3851
+ return value;
3852
+ }
3853
+ const prefix = scheme.charAt(0).toUpperCase() + scheme.slice(1);
3854
+ return value.toLowerCase().startsWith(`${scheme} `) ? value : `${prefix} ${value}`;
3855
+ }
3856
+ if (security.type === "oauth2" || security.type === "openIdConnect") {
3857
+ return value.toLowerCase().startsWith("bearer ") ? value : `Bearer ${value}`;
3858
+ }
3859
+ return value;
3860
+ }
3861
+ function resolveServerUrl(tool) {
3862
+ const server = tool.metadata.servers?.[0];
3863
+ if (!server) return "";
3864
+ let url = server.url;
3865
+ if (server.variables) {
3866
+ for (const [name, variable] of Object.entries(server.variables)) {
3867
+ if (variable && typeof variable.default === "string") {
3868
+ url = url.replaceAll(`{${name}}`, variable.default);
3869
+ }
3870
+ }
3871
+ }
3872
+ return url;
3873
+ }
3874
+ var JSON_CONTENT = /^application\/(.+\+)?json$/i;
3875
+ function buildHttpRequest(tool, input, options = {}) {
3876
+ const rawBase = options.baseUrl ?? resolveServerUrl(tool);
3877
+ if (rawBase.includes("{")) {
3878
+ throw new RequestBuildError(
3879
+ `Base URL '${rawBase}' contains unresolved server template variables (no default value in the spec); pass an explicit baseUrl`,
3880
+ { baseUrl: rawBase }
3881
+ );
3882
+ }
3883
+ if (rawBase !== "" && !/^https?:\/\//i.test(rawBase)) {
3884
+ throw new RequestBuildError(`Base URL must be http(s) or empty; received '${rawBase}'`, { baseUrl: rawBase });
3885
+ }
3886
+ let base = rawBase;
3887
+ while (base.endsWith("/")) base = base.slice(0, -1);
3888
+ let path = tool.metadata.path;
3889
+ const queryPairs = [];
3890
+ const query = {};
3891
+ const headers = {};
3892
+ const cookies = {};
3893
+ let rawBody;
3894
+ let bodyObject;
3895
+ let contentType;
3896
+ let hasBody = false;
3897
+ let binaryBody = false;
3898
+ for (const mapper of tool.mapper) {
3899
+ const value = input[mapper.inputKey];
3900
+ if (mapper.security) {
3901
+ if (value === void 0 || value === null) continue;
3902
+ const formatted = formatSecurityValue(mapper, String(value));
3903
+ if (mapper.type === "header") {
3904
+ assertHeaderSafe(mapper.key, formatted);
3905
+ headers[mapper.key] = formatted;
3906
+ } else if (mapper.type === "query") {
3907
+ queryPairs.push([mapper.key, formatted]);
3908
+ } else {
3909
+ assertCookieName(mapper.key);
3910
+ cookies[mapper.key] = formatted;
3911
+ }
3912
+ continue;
3913
+ }
3914
+ if (value === void 0 || value === null && mapper.type !== "body") {
3915
+ if (mapper.required) {
3916
+ throw new RequestBuildError(
3917
+ `Required ${mapper.type} parameter '${mapper.key}' (input key '${mapper.inputKey}') is missing`,
3918
+ { param: mapper.key, inputKey: mapper.inputKey, location: mapper.type }
3919
+ );
3920
+ }
3921
+ continue;
3922
+ }
3923
+ switch (mapper.type) {
3924
+ case "path":
3925
+ path = path.replaceAll(`{${mapper.key}}`, serializePathValue(mapper, value));
3926
+ break;
3927
+ case "query":
3928
+ for (const [k, v] of serializeQueryPairs(mapper, value)) {
3929
+ queryPairs.push([k, v, mapper.allowReserved]);
3930
+ }
3931
+ break;
3932
+ case "header": {
3933
+ const headerValue = serializeHeaderValue(mapper, value);
3934
+ assertHeaderSafe(mapper.key, headerValue);
3935
+ headers[mapper.key] = headerValue;
3936
+ break;
3937
+ }
3938
+ case "cookie": {
3939
+ assertCookieName(mapper.key);
3940
+ cookies[mapper.key] = Array.isArray(value) ? value.map((v) => primitiveString(v, mapper.key, "cookie")).join(",") : primitiveString(value, mapper.key, "cookie");
3941
+ break;
3942
+ }
3943
+ case "body":
3944
+ hasBody = true;
3945
+ contentType = contentType ?? mapper.serialization?.contentType ?? "application/json";
3946
+ if (mapper.serialization?.binary) binaryBody = true;
3947
+ if (mapper.wholeBody) {
3948
+ rawBody = value;
3949
+ } else {
3950
+ if (bodyObject === void 0) bodyObject = {};
3951
+ bodyObject[mapper.key] = value;
3952
+ }
3953
+ break;
3954
+ }
3955
+ }
3956
+ if (path.includes("{")) {
3957
+ throw new RequestBuildError(`Unresolved path parameters remain in '${path}'`, { path });
3958
+ }
3959
+ if (bodyObject !== void 0) rawBody = bodyObject;
3960
+ const queryString = queryPairs.map(([k, v, allowReserved]) => {
3961
+ query[k] = query[k] ?? [];
3962
+ query[k].push(v);
3963
+ const encodedKey = encodeURIComponent(k).replace(/%5B/gi, "[").replace(/%5D/gi, "]");
3964
+ return `${encodedKey}=${encodeValue(v, allowReserved)}`;
3965
+ }).join("&");
3966
+ const cookieEntries = Object.entries(cookies);
3967
+ if (cookieEntries.length > 0) {
3968
+ for (const [k, v] of cookieEntries) assertCookieValue(k, v);
3969
+ headers["Cookie"] = cookieEntries.map(([k, v]) => `${k}=${v}`).join("; ");
3970
+ }
3971
+ const contentTypeKey = Object.keys(headers).find((h) => h.toLowerCase() === "content-type") ?? "content-type";
3972
+ const hasExplicitContentType = contentTypeKey in headers;
3973
+ let body;
3974
+ if (hasBody && rawBody !== void 0) {
3975
+ const ct = contentType;
3976
+ if (binaryBody) {
3977
+ body = rawBody;
3978
+ if (!hasExplicitContentType) headers[contentTypeKey] = ct;
3979
+ } else if (ct.toLowerCase() === "application/x-www-form-urlencoded") {
3980
+ const params = new URLSearchParams();
3981
+ if (!isPlainObject(rawBody)) {
3982
+ throw new RequestBuildError(`form-urlencoded bodies must be objects; received ${typeof rawBody}`, {
3983
+ contentType: ct
3984
+ });
3985
+ }
3986
+ for (const [k, v] of Object.entries(rawBody)) {
3987
+ if (v === void 0) continue;
3988
+ if (Array.isArray(v)) {
3989
+ for (const item of v) params.append(k, primitiveString(item, k, "body"));
3990
+ } else {
3991
+ params.append(k, isPlainObject(v) ? JSON.stringify(v) : String(v));
3992
+ }
3993
+ }
3994
+ body = params.toString();
3995
+ headers[contentTypeKey] = ct;
3996
+ } else if (ct.toLowerCase() === "multipart/form-data") {
3997
+ if (typeof FormData === "undefined") {
3998
+ throw new RequestBuildError("multipart/form-data requires a FormData implementation in this runtime", {});
3999
+ }
4000
+ const form = new FormData();
4001
+ if (!isPlainObject(rawBody)) {
4002
+ throw new RequestBuildError(`multipart bodies must be objects; received ${typeof rawBody}`, {
4003
+ contentType: ct
4004
+ });
4005
+ }
4006
+ for (const [k, v] of Object.entries(rawBody)) {
4007
+ if (v === void 0) continue;
4008
+ if (typeof Blob !== "undefined" && v instanceof Blob) {
4009
+ form.append(k, v);
4010
+ } else if (v instanceof Uint8Array) {
4011
+ form.append(k, new Blob([v]));
4012
+ } else if (isPlainObject(v) || Array.isArray(v)) {
4013
+ form.append(k, JSON.stringify(v));
4014
+ } else {
4015
+ form.append(k, String(v));
4016
+ }
4017
+ }
4018
+ body = form;
4019
+ if (hasExplicitContentType) delete headers[contentTypeKey];
4020
+ } else if (JSON_CONTENT.test(ct)) {
4021
+ body = JSON.stringify(rawBody);
4022
+ headers[contentTypeKey] = ct;
4023
+ } else {
4024
+ body = isPlainObject(rawBody) || Array.isArray(rawBody) ? JSON.stringify(rawBody) : String(rawBody);
4025
+ headers[contentTypeKey] = ct;
4026
+ }
4027
+ }
4028
+ return {
4029
+ url: `${base}${path}${queryString ? `?${queryString}` : ""}`,
4030
+ method: tool.metadata.method.toUpperCase(),
4031
+ headers,
4032
+ query,
4033
+ cookies,
4034
+ contentType,
4035
+ body,
4036
+ rawBody
4037
+ };
4038
+ }
4039
+
4040
+ // src/sdk.ts
4041
+ function toSdkTool(tool, wrapper) {
4042
+ const wrapSchema = wrapper?.fromJsonSchema ?? ((schema) => schema);
4043
+ return [
4044
+ tool.name,
4045
+ {
4046
+ ...tool.title !== void 0 && { title: tool.title },
4047
+ description: tool.description,
4048
+ inputSchema: wrapSchema(tool.inputSchema),
4049
+ ...tool.outputSchema !== void 0 && { outputSchema: wrapSchema(tool.outputSchema) },
4050
+ ...tool.annotations !== void 0 && { annotations: tool.annotations }
4051
+ }
4052
+ ];
4053
+ }
4054
+
4055
+ // src/token-report.ts
4056
+ function estimateToolTokens(tool) {
4057
+ const advertised = {
4058
+ name: tool.name,
4059
+ ...tool.title !== void 0 && { title: tool.title },
4060
+ description: tool.description,
4061
+ ...tool.annotations !== void 0 && { annotations: tool.annotations },
4062
+ inputSchema: tool.inputSchema,
4063
+ ...tool.outputSchema !== void 0 && { outputSchema: tool.outputSchema }
4064
+ };
4065
+ return Math.ceil(JSON.stringify(advertised).length / 4);
4066
+ }
4067
+ function analyzeToolSet(tools, options = {}) {
4068
+ const tokenBudget = options.tokenBudget ?? 1e4;
4069
+ const maxRecommendedTools = options.maxRecommendedTools ?? 40;
4070
+ const perToolWarning = options.perToolWarning ?? 2e3;
4071
+ const perTool = tools.map((tool) => ({ name: tool.name, tokens: estimateToolTokens(tool) })).sort((a, b) => b.tokens - a.tokens || (a.name < b.name ? -1 : 1));
4072
+ const estimatedTokens = perTool.reduce((sum, entry) => sum + entry.tokens, 0);
4073
+ const warnings = [];
4074
+ if (tools.length > maxRecommendedTools) {
4075
+ warnings.push(
4076
+ `${tools.length} tools exceeds the ~${maxRecommendedTools}-tool range where model selection accuracy degrades \u2014 curate with filters (tags, paths, readOnlyOnly) or split into focused servers.`
4077
+ );
4078
+ }
4079
+ if (estimatedTokens > tokenBudget) {
4080
+ warnings.push(
4081
+ `Estimated ${estimatedTokens} tokens of tool definitions exceeds the ${tokenBudget}-token budget \u2014 trim schemas (maxSchemaDepth, maxProperties) or reduce the tool count.`
4082
+ );
4083
+ }
4084
+ const heavy = perTool.filter((entry) => entry.tokens > perToolWarning);
4085
+ if (heavy.length > 0) {
4086
+ warnings.push(
4087
+ `${heavy.length} tool(s) exceed ${perToolWarning} tokens each (${heavy.slice(0, 3).map((entry) => `${entry.name}: ~${entry.tokens}`).join(", ")}${heavy.length > 3 ? ", \u2026" : ""}) \u2014 consider schema trimming for these.`
4088
+ );
4089
+ }
4090
+ return { toolCount: tools.length, estimatedTokens, perTool, warnings };
4091
+ }
2138
4092
  // Annotate the CommonJS export names for ESM import in node:
2139
4093
  0 && (module.exports = {
2140
4094
  BLOCKED_HOSTNAMES,
@@ -2143,8 +4097,10 @@ function createSecurityContext(auth) {
2143
4097
  LoadError,
2144
4098
  OpenAPIToolError,
2145
4099
  OpenAPIToolGenerator,
4100
+ OverlayError,
2146
4101
  ParameterResolver,
2147
4102
  ParseError,
4103
+ RequestBuildError,
2148
4104
  ResponseBuilder,
2149
4105
  SchemaBuilder,
2150
4106
  SchemaError,
@@ -2152,15 +4108,32 @@ function createSecurityContext(auth) {
2152
4108
  SsrfError,
2153
4109
  ValidationError,
2154
4110
  Validator,
4111
+ analyzeToolSet,
4112
+ applyClientTarget,
4113
+ applyOverlay,
2155
4114
  assertUrlSafe,
4115
+ buildHttpRequest,
4116
+ collapseNestedUnions,
4117
+ collapseRootCompositions,
2156
4118
  createSecurityContext,
2157
4119
  decodeIpv4MappedIpv6,
2158
4120
  defaultLookup,
4121
+ demoteFormats,
4122
+ enforceClosedObjects,
4123
+ ensureArrayItems,
4124
+ estimateToolTokens,
4125
+ extractExtensionOverrides,
4126
+ inferAnnotationsFromMethod,
4127
+ inlineLocalRefs,
2159
4128
  isBlockedAddress,
2160
4129
  isBlockedHostname,
2161
4130
  isReferenceObject,
4131
+ lintDocument,
2162
4132
  normalizeSsrfOptions,
4133
+ requireAllProperties,
4134
+ resolveExtensionEnabled,
2163
4135
  resolveSchemaFormats,
2164
4136
  safeFetch,
2165
- toJsonSchema
4137
+ toJsonSchema,
4138
+ toSdkTool
2166
4139
  });