mcp-from-openapi 2.5.1 → 2.6.0

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