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