mcp-from-openapi 2.6.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/index.mjs CHANGED
@@ -930,6 +930,96 @@ var SchemaBuilder = class {
930
930
  }
931
931
  return copy;
932
932
  }
933
+ // Copy-on-walk over every structural keyword (same key groups as
934
+ // truncateDepth): `visit` transforms each node top-down and must return a
935
+ // new node when it changes anything.
936
+ static walkCopy(node, visit, seen = /* @__PURE__ */ new Map()) {
937
+ if (!node || typeof node !== "object") return node;
938
+ const existing = seen.get(node);
939
+ if (existing) return existing;
940
+ const copy = visit({ ...node });
941
+ seen.set(node, copy);
942
+ for (const key of this.TRUNCATE_MAP_KEYS) {
943
+ const value = copy[key];
944
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
945
+ const mapped = {};
946
+ for (const [name, sub] of Object.entries(value)) {
947
+ mapped[name] = this.walkCopy(sub, visit, seen);
948
+ }
949
+ copy[key] = mapped;
950
+ }
951
+ }
952
+ for (const key of this.TRUNCATE_SCHEMA_KEYS) {
953
+ const value = copy[key];
954
+ if (Array.isArray(value)) {
955
+ copy[key] = value.map((item) => this.walkCopy(item, visit, seen));
956
+ } else if (value !== null && typeof value === "object") {
957
+ copy[key] = this.walkCopy(value, visit, seen);
958
+ }
959
+ }
960
+ for (const key of this.TRUNCATE_LIST_KEYS) {
961
+ const value = copy[key];
962
+ if (Array.isArray(value)) {
963
+ copy[key] = value.map((member) => this.walkCopy(member, visit, seen));
964
+ }
965
+ }
966
+ return copy;
967
+ }
968
+ /**
969
+ * Limit every object node to its first `max` properties (declaration
970
+ * order). Dropped properties are pruned from `required` and counted in a
971
+ * note appended to the node's description.
972
+ */
973
+ static limitProperties(schema, max) {
974
+ const bound = Number.isFinite(max) ? Math.max(1, Math.floor(max)) : Number.MAX_SAFE_INTEGER;
975
+ return this.walkCopy(schema, (node) => {
976
+ const properties = node.properties;
977
+ if (!properties || typeof properties !== "object") return node;
978
+ const entries = Object.entries(properties);
979
+ if (entries.length <= bound) return node;
980
+ const kept = entries.slice(0, bound);
981
+ const keptNames = new Set(kept.map(([name]) => name));
982
+ const dropped = entries.length - bound;
983
+ const note = `[${dropped} additional propert${dropped === 1 ? "y" : "ies"} omitted: exceeds maxProperties]`;
984
+ const next = { ...node, properties: Object.fromEntries(kept) };
985
+ if (Array.isArray(node.required)) {
986
+ const required = node.required.filter((name) => keptNames.has(String(name)));
987
+ if (required.length > 0) {
988
+ next.required = required;
989
+ } else {
990
+ delete next.required;
991
+ }
992
+ }
993
+ next.description = node.description ? `${node.description} ${note}` : note;
994
+ return next;
995
+ });
996
+ }
997
+ /**
998
+ * Cap every description in the schema tree to `maxLength` characters,
999
+ * truncating with an ellipsis.
1000
+ */
1001
+ static capDescriptions(schema, maxLength) {
1002
+ const bound = Number.isFinite(maxLength) ? Math.max(1, Math.floor(maxLength)) : Number.MAX_SAFE_INTEGER;
1003
+ return this.walkCopy(schema, (node) => {
1004
+ if (typeof node.description === "string" && node.description.length > bound) {
1005
+ return { ...node, description: `${node.description.slice(0, bound - 1)}\u2026` };
1006
+ }
1007
+ return node;
1008
+ });
1009
+ }
1010
+ /**
1011
+ * Remove every `examples` array from the schema tree (a token-budget
1012
+ * trimming step — validation keywords are untouched).
1013
+ */
1014
+ static stripExamples(schema) {
1015
+ return this.walkCopy(schema, (node) => {
1016
+ if ("examples" in node) {
1017
+ const { examples: _examples, ...rest } = node;
1018
+ return rest;
1019
+ }
1020
+ return node;
1021
+ });
1022
+ }
933
1023
  /**
934
1024
  * Simplify schema by removing unnecessary fields
935
1025
  */
@@ -1370,6 +1460,557 @@ function applyClientTarget(schema, target) {
1370
1460
  return result;
1371
1461
  }
1372
1462
 
1463
+ // src/errors.ts
1464
+ var OpenAPIToolError = class extends Error {
1465
+ context;
1466
+ constructor(message, context) {
1467
+ super(message);
1468
+ this.name = this.constructor.name;
1469
+ this.context = context;
1470
+ if (Error.captureStackTrace) {
1471
+ Error.captureStackTrace(this, this.constructor);
1472
+ }
1473
+ }
1474
+ };
1475
+ var LoadError = class extends OpenAPIToolError {
1476
+ constructor(message, context) {
1477
+ super(message, context);
1478
+ }
1479
+ };
1480
+ var SsrfError = class extends LoadError {
1481
+ constructor(message, context) {
1482
+ super(message, context);
1483
+ }
1484
+ };
1485
+ var ParseError = class extends OpenAPIToolError {
1486
+ constructor(message, context) {
1487
+ super(message, context);
1488
+ }
1489
+ };
1490
+ var ValidationError = class extends OpenAPIToolError {
1491
+ errors;
1492
+ constructor(message, context) {
1493
+ super(message, context);
1494
+ this.errors = context?.["errors"];
1495
+ }
1496
+ };
1497
+ var GenerationError = class extends OpenAPIToolError {
1498
+ constructor(message, context) {
1499
+ super(message, context);
1500
+ }
1501
+ };
1502
+ var OverlayError = class extends OpenAPIToolError {
1503
+ constructor(message, context) {
1504
+ super(message, context);
1505
+ }
1506
+ };
1507
+ var RequestBuildError = class extends OpenAPIToolError {
1508
+ constructor(message, context) {
1509
+ super(message, context);
1510
+ }
1511
+ };
1512
+ var SchemaError = class extends OpenAPIToolError {
1513
+ constructor(message, context) {
1514
+ super(message, context);
1515
+ }
1516
+ };
1517
+
1518
+ // src/overlay.ts
1519
+ function parsePath(path) {
1520
+ if (typeof path !== "string" || !path.startsWith("$")) {
1521
+ throw new OverlayError(`Overlay target must be a JSONPath starting with '$'; received '${String(path)}'`, {
1522
+ target: path
1523
+ });
1524
+ }
1525
+ const segments = [];
1526
+ let rest = path.slice(1);
1527
+ while (rest.length > 0) {
1528
+ let recursive = false;
1529
+ if (rest.startsWith("..")) {
1530
+ recursive = true;
1531
+ rest = rest.slice(2);
1532
+ const bare = rest.match(/^([A-Za-z_][\w-]*)/);
1533
+ if (bare) {
1534
+ segments.push({ kind: "child", name: bare[1], recursive });
1535
+ rest = rest.slice(bare[0].length);
1536
+ continue;
1537
+ }
1538
+ } else if (rest.startsWith(".")) {
1539
+ rest = rest.slice(1);
1540
+ if (rest.startsWith("*")) {
1541
+ segments.push({ kind: "wildcard", recursive });
1542
+ rest = rest.slice(1);
1543
+ continue;
1544
+ }
1545
+ const bare = rest.match(/^([A-Za-z_][\w-]*)/);
1546
+ if (bare) {
1547
+ segments.push({ kind: "child", name: bare[1], recursive });
1548
+ rest = rest.slice(bare[0].length);
1549
+ continue;
1550
+ }
1551
+ throw new OverlayError(`Invalid JSONPath segment after '.' in '${path}'`, { target: path });
1552
+ }
1553
+ if (!rest.startsWith("[")) {
1554
+ throw new OverlayError(`Invalid JSONPath segment at '${rest}' in '${path}'`, { target: path });
1555
+ }
1556
+ const bracket = matchBracket(rest, path);
1557
+ const inner = bracket.inner.trim();
1558
+ rest = bracket.rest;
1559
+ if (inner === "*") {
1560
+ segments.push({ kind: "wildcard", recursive });
1561
+ } else if (/^-?\d+$/.test(inner)) {
1562
+ segments.push({ kind: "index", index: parseInt(inner, 10), recursive });
1563
+ } else if (/^'.*'$/.test(inner) || /^".*"$/.test(inner)) {
1564
+ segments.push({ kind: "child", name: inner.slice(1, -1), recursive });
1565
+ } else if (inner.startsWith("?(") && inner.endsWith(")")) {
1566
+ segments.push(parseFilter(inner.slice(2, -1).trim(), path, recursive));
1567
+ } else {
1568
+ throw new OverlayError(`Unsupported JSONPath selector '[${inner}]' in '${path}'`, { target: path });
1569
+ }
1570
+ }
1571
+ return segments;
1572
+ }
1573
+ function matchBracket(input, fullPath) {
1574
+ let quote = null;
1575
+ let depth = 0;
1576
+ for (let i = 1; i < input.length; i++) {
1577
+ const char = input[i];
1578
+ if (quote) {
1579
+ if (char === quote) quote = null;
1580
+ } else if (char === "'" || char === '"') {
1581
+ quote = char;
1582
+ } else if (char === "[") {
1583
+ depth++;
1584
+ } else if (char === "]") {
1585
+ if (depth === 0) {
1586
+ return { inner: input.slice(1, i), rest: input.slice(i + 1) };
1587
+ }
1588
+ depth--;
1589
+ }
1590
+ }
1591
+ throw new OverlayError(`Unterminated '[' selector in '${fullPath}'`, { target: fullPath });
1592
+ }
1593
+ function parseFilter(expr, path, recursive) {
1594
+ const match = expr.match(/^@(?:\.([A-Za-z_][\w-]*)|\['([^']*)'\]|\["([^"]*)"\])\s*(?:(==|!=)\s*(.+))?$/);
1595
+ if (!match) {
1596
+ throw new OverlayError(`Unsupported filter expression '?(${expr})' in '${path}'`, { target: path });
1597
+ }
1598
+ const field = match[1] ?? match[2] ?? match[3];
1599
+ const op = match[4];
1600
+ if (!op) {
1601
+ return { kind: "filter", field, op: "exists", recursive };
1602
+ }
1603
+ const raw = match[5].trim();
1604
+ let literal;
1605
+ if (/^'.*'$/.test(raw) || /^".*"$/.test(raw)) {
1606
+ literal = raw.slice(1, -1);
1607
+ } else if (/^-?\d+(\.\d+)?$/.test(raw)) {
1608
+ literal = parseFloat(raw);
1609
+ } else if (raw === "true" || raw === "false") {
1610
+ literal = raw === "true";
1611
+ } else {
1612
+ throw new OverlayError(`Unsupported filter literal '${raw}' in '${path}'`, { target: path });
1613
+ }
1614
+ return { kind: "filter", field, op, literal, recursive };
1615
+ }
1616
+ function isContainer(value) {
1617
+ return value !== null && typeof value === "object";
1618
+ }
1619
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1620
+ function descendants(match) {
1621
+ const result = [];
1622
+ const walk = (node) => {
1623
+ if (!isContainer(node)) return;
1624
+ if (Array.isArray(node)) {
1625
+ node.forEach((item, index) => {
1626
+ result.push({ parent: node, key: index, value: item });
1627
+ walk(item);
1628
+ });
1629
+ } else {
1630
+ for (const [key, value] of Object.entries(node)) {
1631
+ result.push({ parent: node, key, value });
1632
+ walk(value);
1633
+ }
1634
+ }
1635
+ };
1636
+ walk(match.value);
1637
+ return result;
1638
+ }
1639
+ function dedupeMatches(matches) {
1640
+ const seen = /* @__PURE__ */ new Map();
1641
+ const result = [];
1642
+ for (const match of matches) {
1643
+ let keys = seen.get(match.parent);
1644
+ if (!keys) {
1645
+ keys = /* @__PURE__ */ new Set();
1646
+ seen.set(match.parent, keys);
1647
+ }
1648
+ if (keys.has(match.key)) continue;
1649
+ keys.add(match.key);
1650
+ result.push(match);
1651
+ }
1652
+ return result;
1653
+ }
1654
+ function applySegment(matches, segment) {
1655
+ const scope = segment.recursive ? matches.flatMap((m) => [m, ...descendants(m)]) : matches;
1656
+ const next = [];
1657
+ for (const match of scope) {
1658
+ const node = match.value;
1659
+ switch (segment.kind) {
1660
+ case "child": {
1661
+ if (isContainer(node) && !Array.isArray(node) && !UNSAFE_KEYS.has(segment.name) && Object.prototype.hasOwnProperty.call(node, segment.name)) {
1662
+ next.push({ parent: node, key: segment.name, value: node[segment.name] });
1663
+ }
1664
+ break;
1665
+ }
1666
+ case "wildcard": {
1667
+ if (Array.isArray(node)) {
1668
+ node.forEach((item, index) => next.push({ parent: node, key: index, value: item }));
1669
+ } else if (isContainer(node)) {
1670
+ for (const [key, value] of Object.entries(node)) {
1671
+ next.push({ parent: node, key, value });
1672
+ }
1673
+ }
1674
+ break;
1675
+ }
1676
+ case "index": {
1677
+ if (Array.isArray(node)) {
1678
+ const index = segment.index < 0 ? node.length + segment.index : segment.index;
1679
+ if (index >= 0 && index < node.length) {
1680
+ next.push({ parent: node, key: index, value: node[index] });
1681
+ }
1682
+ }
1683
+ break;
1684
+ }
1685
+ case "filter": {
1686
+ const members = Array.isArray(node) ? node.map((item, index) => ({ parent: node, key: index, value: item })) : isContainer(node) ? Object.entries(node).map(([key, value]) => ({ parent: node, key, value })) : [];
1687
+ for (const member of members) {
1688
+ if (!isContainer(member.value) || Array.isArray(member.value)) continue;
1689
+ const fieldValue = member.value[segment.field];
1690
+ const keep = segment.op === "exists" ? fieldValue !== void 0 : segment.op === "==" ? fieldValue === segment.literal : fieldValue !== segment.literal;
1691
+ if (keep) next.push(member);
1692
+ }
1693
+ break;
1694
+ }
1695
+ }
1696
+ }
1697
+ return next;
1698
+ }
1699
+ function deepMerge(target, update) {
1700
+ for (const [key, value] of Object.entries(update)) {
1701
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
1702
+ const existing = target[key];
1703
+ if (isContainer(value) && !Array.isArray(value) && isContainer(existing) && !Array.isArray(existing)) {
1704
+ deepMerge(existing, value);
1705
+ } else {
1706
+ target[key] = value;
1707
+ }
1708
+ }
1709
+ }
1710
+ function applyOverlay(document, overlay) {
1711
+ if (!overlay || typeof overlay !== "object" || !Array.isArray(overlay.actions)) {
1712
+ throw new OverlayError("Overlay document must have an actions array", {});
1713
+ }
1714
+ const result = JSON.parse(JSON.stringify(document));
1715
+ for (const [index, action] of overlay.actions.entries()) {
1716
+ if (!action || typeof action !== "object" || typeof action.target !== "string") {
1717
+ throw new OverlayError(`Overlay action #${index} must have a string target`, { index });
1718
+ }
1719
+ if (action.update === void 0 && action.remove !== true) {
1720
+ throw new OverlayError(`Overlay action #${index} needs 'update' or 'remove: true'`, {
1721
+ index,
1722
+ target: action.target
1723
+ });
1724
+ }
1725
+ const segments = parsePath(action.target);
1726
+ let matches = [{ parent: null, key: null, value: result }];
1727
+ for (const segment of segments) {
1728
+ matches = dedupeMatches(applySegment(matches, segment));
1729
+ }
1730
+ if (action.remove === true) {
1731
+ const arrayRemovals = /* @__PURE__ */ new Map();
1732
+ for (const match of matches) {
1733
+ if (match.parent === null) {
1734
+ throw new OverlayError("Overlay cannot remove the document root", { target: action.target });
1735
+ }
1736
+ if (Array.isArray(match.parent)) {
1737
+ const indices = arrayRemovals.get(match.parent) ?? [];
1738
+ indices.push(match.key);
1739
+ arrayRemovals.set(match.parent, indices);
1740
+ } else {
1741
+ delete match.parent[match.key];
1742
+ }
1743
+ }
1744
+ for (const [parent, indices] of arrayRemovals) {
1745
+ for (const index2 of indices.sort((a, b) => b - a)) {
1746
+ parent.splice(index2, 1);
1747
+ }
1748
+ }
1749
+ continue;
1750
+ }
1751
+ for (const match of matches) {
1752
+ const node = match.value;
1753
+ if (Array.isArray(node)) {
1754
+ node.push(action.update);
1755
+ } else if (isContainer(node) && isContainer(action.update) && !Array.isArray(action.update)) {
1756
+ deepMerge(node, action.update);
1757
+ } else {
1758
+ if (match.parent === null) {
1759
+ throw new OverlayError("Overlay cannot replace the document root with a non-object", {
1760
+ target: action.target
1761
+ });
1762
+ }
1763
+ match.parent[match.key] = action.update;
1764
+ }
1765
+ }
1766
+ }
1767
+ return result;
1768
+ }
1769
+
1770
+ // src/lint.ts
1771
+ var METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
1772
+ var PAGINATION_PARAM = /^(page|limit|offset|cursor|per_page|pagesize|page_size|after|before)$/i;
1773
+ var DEEP_SCHEMA_THRESHOLD = 8;
1774
+ var WIDE_SCHEMA_THRESHOLD = 30;
1775
+ function measureSchema(node, seen = /* @__PURE__ */ new Map()) {
1776
+ if (node === null || typeof node !== "object") {
1777
+ return { depth: 0, widestObject: 0, hasArray: false };
1778
+ }
1779
+ if (seen.has(node)) {
1780
+ return seen.get(node) ?? { depth: 0, widestObject: 0, hasArray: false };
1781
+ }
1782
+ seen.set(node, null);
1783
+ const record = node;
1784
+ let childDepth = 0;
1785
+ let widestObject = 0;
1786
+ let hasArray = record["type"] === "array" || Array.isArray(record["type"]) && record["type"].includes("array");
1787
+ const visit = (child) => {
1788
+ const shape2 = measureSchema(child, seen);
1789
+ childDepth = Math.max(childDepth, shape2.depth);
1790
+ widestObject = Math.max(widestObject, shape2.widestObject);
1791
+ hasArray = hasArray || shape2.hasArray;
1792
+ };
1793
+ const properties = record["properties"];
1794
+ if (properties && typeof properties === "object") {
1795
+ widestObject = Math.max(widestObject, Object.keys(properties).length);
1796
+ for (const child of Object.values(properties)) visit(child);
1797
+ }
1798
+ for (const key of ["items", "additionalProperties", "not", "contentSchema"]) {
1799
+ const value = record[key];
1800
+ if (value && typeof value === "object" && !Array.isArray(value)) visit(value);
1801
+ if (Array.isArray(value)) value.forEach(visit);
1802
+ }
1803
+ for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
1804
+ const value = record[key];
1805
+ if (Array.isArray(value)) value.forEach(visit);
1806
+ }
1807
+ const shape = { depth: childDepth + 1, widestObject, hasArray };
1808
+ seen.set(node, shape);
1809
+ return shape;
1810
+ }
1811
+ function schemaHasExample(node, seen = /* @__PURE__ */ new Set()) {
1812
+ if (node === null || typeof node !== "object" || seen.has(node)) return false;
1813
+ seen.add(node);
1814
+ const record = node;
1815
+ if (record["example"] !== void 0 || record["examples"] !== void 0) return true;
1816
+ const properties = record["properties"];
1817
+ if (properties && typeof properties === "object") {
1818
+ if (Object.values(properties).some((child) => schemaHasExample(child, seen))) return true;
1819
+ }
1820
+ for (const key of ["items", "additionalProperties", "not", "contentSchema"]) {
1821
+ const value = record[key];
1822
+ if (value && typeof value === "object" && !Array.isArray(value) && schemaHasExample(value, seen)) return true;
1823
+ if (Array.isArray(value) && value.some((item) => schemaHasExample(item, seen))) return true;
1824
+ }
1825
+ for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
1826
+ const value = record[key];
1827
+ if (Array.isArray(value) && value.some((member) => schemaHasExample(member, seen))) return true;
1828
+ }
1829
+ return false;
1830
+ }
1831
+ function hasAnyExample(content) {
1832
+ if (!content) return false;
1833
+ return Object.values(content).some((media) => {
1834
+ if (!media || typeof media !== "object") return false;
1835
+ const record = media;
1836
+ if (record["example"] !== void 0 || record["examples"] !== void 0) return true;
1837
+ return schemaHasExample(record["schema"]);
1838
+ });
1839
+ }
1840
+ function lintDocument(document) {
1841
+ const findings = [];
1842
+ const operationIds = /* @__PURE__ */ new Map();
1843
+ const paths = document.paths ?? {};
1844
+ for (const [pathStr, pathItem] of Object.entries(paths).sort(([a], [b]) => a < b ? -1 : 1)) {
1845
+ if (!pathItem || "$ref" in pathItem) continue;
1846
+ const pathLevelParameters = (pathItem["parameters"] ?? []).filter(
1847
+ (param) => !isReferenceObject(param)
1848
+ );
1849
+ for (const method of METHODS) {
1850
+ const operation = pathItem[method];
1851
+ if (!operation) continue;
1852
+ const label = `${method.toUpperCase()} ${pathStr}`;
1853
+ if (!operation.operationId) {
1854
+ findings.push({
1855
+ severity: "warning",
1856
+ code: "missing-operation-id",
1857
+ message: "Operation has no operationId; the tool name will be generated from the method and path.",
1858
+ path: label,
1859
+ hint: "Add a short, action-oriented operationId (it becomes the tool name)."
1860
+ });
1861
+ } else {
1862
+ const existing = operationIds.get(operation.operationId) ?? [];
1863
+ existing.push(label);
1864
+ operationIds.set(operation.operationId, existing);
1865
+ if (operation.operationId.length > 64) {
1866
+ findings.push({
1867
+ severity: "info",
1868
+ code: "long-operation-id",
1869
+ message: `operationId '${operation.operationId.slice(0, 40)}\u2026' exceeds 64 characters and will be truncated with a hash suffix.`,
1870
+ path: label,
1871
+ hint: "Shorten the operationId below 64 characters to keep tool names readable."
1872
+ });
1873
+ }
1874
+ }
1875
+ const prose = `${operation.summary ?? ""} ${operation.description ?? ""}`.trim();
1876
+ if (prose.length === 0) {
1877
+ findings.push({
1878
+ severity: "warning",
1879
+ code: "missing-description",
1880
+ message: "Operation has neither summary nor description; the model only sees the method and path.",
1881
+ path: label,
1882
+ hint: "Describe WHEN to use this operation and what it returns (or patch it in with an overlay)."
1883
+ });
1884
+ } else if (prose.length < 20) {
1885
+ findings.push({
1886
+ severity: "info",
1887
+ code: "vague-description",
1888
+ message: `Operation description is only ${prose.length} characters \u2014 likely too vague for reliable tool selection.`,
1889
+ path: label,
1890
+ hint: "Expand the description with the use case and key parameters."
1891
+ });
1892
+ }
1893
+ const parameters = [
1894
+ ...pathLevelParameters,
1895
+ ...(operation.parameters ?? []).filter((param) => !isReferenceObject(param))
1896
+ ];
1897
+ const undescribed = parameters.filter((param) => !param.description).map((param) => param.name);
1898
+ if (undescribed.length > 0) {
1899
+ findings.push({
1900
+ severity: "info",
1901
+ code: "missing-parameter-description",
1902
+ message: `Parameter(s) without description: ${undescribed.join(", ")}.`,
1903
+ path: label,
1904
+ hint: "Describe each parameter \u2014 models mis-fill undocumented arguments."
1905
+ });
1906
+ }
1907
+ const responses = operation.responses ?? {};
1908
+ const successCodes = Object.keys(responses).filter((code) => /^2(\d\d|XX)$/i.test(code));
1909
+ if (successCodes.length === 0 && !responses["default"]) {
1910
+ findings.push({
1911
+ severity: "warning",
1912
+ code: "missing-success-response",
1913
+ message: "Operation declares no 2xx or default response; no output schema can be generated.",
1914
+ path: label,
1915
+ hint: "Add the success response with its schema."
1916
+ });
1917
+ }
1918
+ let responseShape = { depth: 0, widestObject: 0, hasArray: false };
1919
+ for (const code of [...successCodes, "default"]) {
1920
+ const response = responses[code];
1921
+ if (!response || typeof response !== "object" || isReferenceObject(response)) continue;
1922
+ const content = response["content"];
1923
+ if (!content) continue;
1924
+ for (const media of Object.values(content)) {
1925
+ const schema = media && typeof media === "object" ? media["schema"] : void 0;
1926
+ const shape = measureSchema(schema);
1927
+ responseShape = {
1928
+ depth: Math.max(responseShape.depth, shape.depth),
1929
+ widestObject: Math.max(responseShape.widestObject, shape.widestObject),
1930
+ hasArray: responseShape.hasArray || shape.hasArray
1931
+ };
1932
+ }
1933
+ }
1934
+ if (method === "get" && responseShape.hasArray) {
1935
+ const hasPagination = parameters.some((param) => param.in === "query" && PAGINATION_PARAM.test(param.name));
1936
+ if (!hasPagination) {
1937
+ findings.push({
1938
+ severity: "warning",
1939
+ code: "unpaginated-list",
1940
+ message: "GET returns an array but declares no pagination parameter \u2014 responses can blow past client result limits (Claude Code caps tool results at 25K tokens).",
1941
+ path: label,
1942
+ hint: "Add limit/cursor/page parameters, or shape responses at the server."
1943
+ });
1944
+ }
1945
+ }
1946
+ const body = operation.requestBody;
1947
+ const bodyContent = body && !isReferenceObject(body) ? body.content : void 0;
1948
+ let requestShape = { depth: 0, widestObject: 0, hasArray: false };
1949
+ for (const media of Object.values(bodyContent ?? {})) {
1950
+ const schema = media && typeof media === "object" ? media["schema"] : void 0;
1951
+ const shape = measureSchema(schema);
1952
+ requestShape = {
1953
+ depth: Math.max(requestShape.depth, shape.depth),
1954
+ widestObject: Math.max(requestShape.widestObject, shape.widestObject),
1955
+ hasArray: requestShape.hasArray || shape.hasArray
1956
+ };
1957
+ }
1958
+ const maxDepth = Math.max(requestShape.depth, responseShape.depth);
1959
+ if (maxDepth > DEEP_SCHEMA_THRESHOLD) {
1960
+ findings.push({
1961
+ severity: "warning",
1962
+ code: "deep-schema",
1963
+ message: `Schema nesting reaches depth ${maxDepth} (threshold ${DEEP_SCHEMA_THRESHOLD}) \u2014 deep schemas cost tokens and reduce accuracy.`,
1964
+ path: label,
1965
+ hint: "Flatten the schema, or bound generation with maxSchemaDepth."
1966
+ });
1967
+ }
1968
+ const maxWidth = Math.max(requestShape.widestObject, responseShape.widestObject);
1969
+ if (maxWidth > WIDE_SCHEMA_THRESHOLD) {
1970
+ findings.push({
1971
+ severity: "info",
1972
+ code: "wide-schema",
1973
+ message: `An object schema declares ${maxWidth} properties (threshold ${WIDE_SCHEMA_THRESHOLD}).`,
1974
+ path: label,
1975
+ hint: "Split the payload, or bound generation with maxProperties."
1976
+ });
1977
+ }
1978
+ if (bodyContent && !hasAnyExample(bodyContent)) {
1979
+ findings.push({
1980
+ severity: "info",
1981
+ code: "missing-request-example",
1982
+ message: "Request body has no example \u2014 examples measurably improve complex-parameter accuracy.",
1983
+ path: label,
1984
+ hint: "Add a media-type example (and enable includeExamples), or patch one in with an overlay."
1985
+ });
1986
+ }
1987
+ }
1988
+ }
1989
+ for (const [operationId, labels] of operationIds) {
1990
+ if (labels.length > 1) {
1991
+ findings.push({
1992
+ severity: "error",
1993
+ code: "duplicate-operation-id",
1994
+ message: `operationId '${operationId}' is used by ${labels.length} operations: ${labels.join(", ")}.`,
1995
+ path: labels[0],
1996
+ hint: "Make operationIds unique \u2014 duplicates force hash-suffixed tool names."
1997
+ });
1998
+ }
1999
+ }
2000
+ const rank = { error: 0, warning: 1, info: 2 };
2001
+ findings.sort(
2002
+ (a, b) => rank[a.severity] - rank[b.severity] || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0) || (a.code < b.code ? -1 : 1)
2003
+ );
2004
+ return {
2005
+ findings,
2006
+ counts: {
2007
+ error: findings.filter((f) => f.severity === "error").length,
2008
+ warning: findings.filter((f) => f.severity === "warning").length,
2009
+ info: findings.filter((f) => f.severity === "info").length
2010
+ }
2011
+ };
2012
+ }
2013
+
1373
2014
  // src/validator.ts
1374
2015
  var Validator = class {
1375
2016
  /**
@@ -1501,7 +2142,7 @@ var Validator = class {
1501
2142
  if (operation.parameters) {
1502
2143
  this.validateParameters(operation.parameters, path, method, errors, warnings);
1503
2144
  }
1504
- const pathParams = path.match(/\{([^}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
2145
+ const pathParams = path.match(/\{([^{}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
1505
2146
  const definedPathParams = new Set(
1506
2147
  operation.parameters?.filter((p) => p.in === "path").map((p) => p.name) ?? []
1507
2148
  );
@@ -1561,56 +2202,6 @@ var Validator = class {
1561
2202
  }
1562
2203
  };
1563
2204
 
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);
1573
- }
1574
- }
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
2205
  // src/format-resolver.ts
1615
2206
  var BUILTIN_FORMAT_RESOLVERS = {
1616
2207
  // String formats
@@ -2009,6 +2600,103 @@ function applySecureDefaults(options) {
2009
2600
  }
2010
2601
  };
2011
2602
  }
2603
+ function hasUnboundedArray(node, seen = /* @__PURE__ */ new Set()) {
2604
+ if (node === null || typeof node !== "object" || seen.has(node)) return false;
2605
+ seen.add(node);
2606
+ const record = node;
2607
+ const type = record["type"];
2608
+ const isArray = type === "array" || Array.isArray(type) && type.includes("array");
2609
+ if (isArray && record["maxItems"] === void 0) return true;
2610
+ const children = [];
2611
+ const properties = record["properties"];
2612
+ if (properties && typeof properties === "object") children.push(...Object.values(properties));
2613
+ for (const key of ["items", "additionalProperties", "contentSchema"]) {
2614
+ const value = record[key];
2615
+ if (Array.isArray(value)) children.push(...value);
2616
+ else if (value && typeof value === "object") children.push(value);
2617
+ }
2618
+ for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
2619
+ if (Array.isArray(record[key])) children.push(...record[key]);
2620
+ }
2621
+ return children.some((child) => hasUnboundedArray(child, seen));
2622
+ }
2623
+ function detectResponseHints(outputSchema, mapper) {
2624
+ const paginationParams = [
2625
+ ...new Set(mapper.filter((m) => m.type === "query" && !m.security && PAGINATION_PARAM.test(m.key)).map((m) => m.key))
2626
+ ];
2627
+ const unboundedArray = outputSchema !== void 0 && hasUnboundedArray(outputSchema);
2628
+ if (!unboundedArray && paginationParams.length === 0) return void 0;
2629
+ return {
2630
+ ...unboundedArray && { unboundedArray: true },
2631
+ ...paginationParams.length > 0 && { paginationParams },
2632
+ ...unboundedArray && paginationParams.length === 0 && { largeResponseRisk: true }
2633
+ };
2634
+ }
2635
+ function composeDescription(operation, method, pathStr, strategy) {
2636
+ const fallback = `${method.toUpperCase()} ${pathStr}`;
2637
+ const summary = operation.summary?.trim();
2638
+ const description = operation.description?.trim();
2639
+ switch (strategy) {
2640
+ case "descriptionOnly":
2641
+ return description || summary || fallback;
2642
+ case "combined":
2643
+ if (summary && description && summary !== description) {
2644
+ return `${summary}
2645
+
2646
+ ${description}`;
2647
+ }
2648
+ return summary || description || fallback;
2649
+ case "full": {
2650
+ const parts = [];
2651
+ if (summary) parts.push(summary);
2652
+ if (description && description !== summary) parts.push(description);
2653
+ if (operation.operationId) parts.push(`Operation: ${operation.operationId}`);
2654
+ parts.push(fallback);
2655
+ return parts.join("\n\n");
2656
+ }
2657
+ default:
2658
+ return summary || description || fallback;
2659
+ }
2660
+ }
2661
+ function propertyNames(schema, cap = 8) {
2662
+ const properties = schema["properties"];
2663
+ if (!properties || typeof properties !== "object") return "";
2664
+ const names = Object.keys(properties);
2665
+ const listed = names.slice(0, cap).join(", ");
2666
+ return names.length > cap ? `${listed}, \u2026` : listed;
2667
+ }
2668
+ function summarizeOutputSchema(schema) {
2669
+ const record = schema;
2670
+ const variants = record["oneOf"];
2671
+ if (Array.isArray(variants) && variants.length > 0) {
2672
+ const first = variants[0];
2673
+ const firstSummary = first && typeof first === "object" ? summarizeOutputSchema(first) : void 0;
2674
+ return firstSummary ? `${firstSummary} (${variants.length} response variants)` : void 0;
2675
+ }
2676
+ const type = record["type"];
2677
+ if (type === "object" || type === void 0 && record["properties"]) {
2678
+ const names = propertyNames(record);
2679
+ return names ? `object with fields: ${names}` : "object";
2680
+ }
2681
+ if (type === "array") {
2682
+ const items = record["items"];
2683
+ if (items && typeof items === "object" && !Array.isArray(items)) {
2684
+ const itemRecord = items;
2685
+ if (itemRecord["type"] === "object" || itemRecord["properties"]) {
2686
+ const names = propertyNames(itemRecord);
2687
+ return names ? `array of objects with fields: ${names}` : "array of objects";
2688
+ }
2689
+ if (typeof itemRecord["type"] === "string") {
2690
+ return `array of ${itemRecord["type"]}`;
2691
+ }
2692
+ }
2693
+ return "array";
2694
+ }
2695
+ if (typeof type === "string" && type !== "null") {
2696
+ return type;
2697
+ }
2698
+ return void 0;
2699
+ }
2012
2700
  function globToRegExp(glob) {
2013
2701
  let pattern = "^";
2014
2702
  for (let i = 0; i < glob.length; i++) {
@@ -2031,6 +2719,13 @@ function globToRegExp(glob) {
2031
2719
  function matchesAnyGlob(path, globs) {
2032
2720
  return globs.some((glob) => globToRegExp(glob).test(path));
2033
2721
  }
2722
+ function trimUnderscores(value) {
2723
+ let start = 0;
2724
+ let end = value.length;
2725
+ while (start < end && value[start] === "_") start++;
2726
+ while (end > start && value[end - 1] === "_") end--;
2727
+ return value.slice(start, end);
2728
+ }
2034
2729
  function fnv1aHex(input) {
2035
2730
  let hash = 2166136261;
2036
2731
  for (let i = 0; i < input.length; i++) {
@@ -2041,7 +2736,7 @@ function fnv1aHex(input) {
2041
2736
  }
2042
2737
  function normalizeToolName(raw, maxLength, fallbackSeed) {
2043
2738
  let hashSeed = raw;
2044
- let name = raw.replace(/[^A-Za-z0-9_.-]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
2739
+ let name = trimUnderscores(raw.replace(/[^A-Za-z0-9_.-]/g, "_").replace(/_+/g, "_"));
2045
2740
  if (name.length === 0) {
2046
2741
  hashSeed = fallbackSeed;
2047
2742
  name = `tool_${fnv1aHex(fallbackSeed)}`;
@@ -2074,8 +2769,15 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2074
2769
  validate: options.validate ?? true,
2075
2770
  followRedirects: options.followRedirects ?? true,
2076
2771
  refResolution: options.refResolution ?? {},
2077
- secureDefaults: options.secureDefaults ?? false
2772
+ secureDefaults: options.secureDefaults ?? false,
2773
+ overlays: options.overlays
2078
2774
  };
2775
+ if (this.options.overlays) {
2776
+ const overlays = Array.isArray(this.options.overlays) ? this.options.overlays : [this.options.overlays];
2777
+ for (const overlay of overlays) {
2778
+ this.document = applyOverlay(this.document, overlay);
2779
+ }
2780
+ }
2079
2781
  }
2080
2782
  /**
2081
2783
  * Create generator from a URL
@@ -2105,7 +2807,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2105
2807
  }
2106
2808
  return new _OpenAPIToolGenerator(document, options);
2107
2809
  } catch (error) {
2108
- if (error instanceof LoadError) {
2810
+ if (error instanceof LoadError || error instanceof OverlayError) {
2109
2811
  throw error;
2110
2812
  }
2111
2813
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -2138,6 +2840,9 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2138
2840
  }
2139
2841
  return new _OpenAPIToolGenerator(document, options);
2140
2842
  } catch (error) {
2843
+ if (error instanceof OverlayError) {
2844
+ throw error;
2845
+ }
2141
2846
  const errorMessage = error instanceof Error ? error.message : String(error);
2142
2847
  throw new LoadError(`Failed to load OpenAPI spec from file: ${errorMessage}`, {
2143
2848
  filePath,
@@ -2153,6 +2858,9 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2153
2858
  const document = yaml.parse(yamlString);
2154
2859
  return new _OpenAPIToolGenerator(document, options);
2155
2860
  } catch (error) {
2861
+ if (error instanceof OverlayError) {
2862
+ throw error;
2863
+ }
2156
2864
  const errorMessage = error instanceof Error ? error.message : String(error);
2157
2865
  throw new ParseError(`Failed to parse YAML: ${errorMessage}`, {
2158
2866
  originalError: error
@@ -2179,6 +2887,16 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2179
2887
  const validator = new Validator();
2180
2888
  return validator.validate(this.document);
2181
2889
  }
2890
+ /**
2891
+ * Lint the loaded document for agent-readiness (missing operationIds,
2892
+ * vague descriptions, unpaginated lists, oversized schemas, ...). Runs
2893
+ * after overlays and dereferencing so findings reflect what tools would
2894
+ * actually be generated from.
2895
+ */
2896
+ async lint() {
2897
+ await this.initialize(false);
2898
+ return lintDocument(this.getDocument());
2899
+ }
2182
2900
  // NOTE: internal/private-address blocking + IPv4-mapped-IPv6 decoding now live
2183
2901
  // in `ssrf.ts` (`isBlockedHostname` / `isBlockedAddress` / `decodeIpv4MappedIpv6`),
2184
2902
  // shared by the spec-URL fetch (`fromURL`) and the `$ref` resolver below, and
@@ -2316,7 +3034,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2316
3034
  /**
2317
3035
  * Initialize the generator (dereference if needed, then validate)
2318
3036
  */
2319
- async initialize() {
3037
+ async initialize(runValidation = this.options.validate) {
2320
3038
  if (this.options.dereference && !this.dereferencedDocument) {
2321
3039
  const cloned = JSON.parse(JSON.stringify(this.document));
2322
3040
  if (!_OpenAPIToolGenerator.hasExternalRefs(cloned)) {
@@ -2334,7 +3052,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2334
3052
  }
2335
3053
  }
2336
3054
  }
2337
- if (this.options.validate) {
3055
+ if (runValidation) {
2338
3056
  const validator = new Validator();
2339
3057
  const documentToValidate = this.dereferencedDocument ?? this.document;
2340
3058
  const result = await validator.validate(documentToValidate);
@@ -2432,7 +3150,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2432
3150
  const outputSchema = responseBuilder.build(operation.responses);
2433
3151
  const overrides = extractExtensionOverrides(operation);
2434
3152
  const name = this.generateToolName(pathStr, method, overrides.name ?? operation.operationId, options);
2435
- const description = overrides.description ?? (operation.summary || operation.description || `${method.toUpperCase()} ${pathStr}`);
3153
+ const description = overrides.description ?? composeDescription(operation, method, pathStr, options.descriptionStrategy ?? "summaryOnly");
2436
3154
  const title = overrides.title ?? operation.summary;
2437
3155
  const inferred = options.inferAnnotations !== false ? inferAnnotationsFromMethod(method.toLowerCase()) : void 0;
2438
3156
  const annotations = inferred || overrides.annotations ? { ...inferred, ...overrides.annotations } : void 0;
@@ -2449,16 +3167,57 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2449
3167
  if (resolvedOutputSchema) {
2450
3168
  resolvedOutputSchema = SchemaBuilder.truncateDepth(resolvedOutputSchema, maxSchemaDepth);
2451
3169
  }
3170
+ const applyTrim = (schema, isInputRoot) => {
3171
+ let trimmed = schema;
3172
+ if (options.stripExamples) trimmed = SchemaBuilder.stripExamples(trimmed);
3173
+ if (options.maxDescriptionLength !== void 0) {
3174
+ trimmed = SchemaBuilder.capDescriptions(trimmed, options.maxDescriptionLength);
3175
+ }
3176
+ if (options.maxProperties !== void 0) {
3177
+ if (isInputRoot) {
3178
+ const properties = trimmed.properties;
3179
+ if (properties && typeof properties === "object") {
3180
+ const limited = {};
3181
+ for (const [key, value] of Object.entries(properties)) {
3182
+ limited[key] = SchemaBuilder.limitProperties(value, options.maxProperties);
3183
+ }
3184
+ trimmed = { ...trimmed, properties: limited };
3185
+ }
3186
+ } else {
3187
+ trimmed = SchemaBuilder.limitProperties(trimmed, options.maxProperties);
3188
+ }
3189
+ }
3190
+ return trimmed;
3191
+ };
3192
+ if (options.stripExamples || options.maxProperties !== void 0 || options.maxDescriptionLength !== void 0) {
3193
+ resolvedInputSchema = applyTrim(resolvedInputSchema, true);
3194
+ if (resolvedOutputSchema) {
3195
+ resolvedOutputSchema = applyTrim(resolvedOutputSchema, false);
3196
+ }
3197
+ }
2452
3198
  if (options.target) {
2453
3199
  resolvedInputSchema = applyClientTarget(resolvedInputSchema, options.target);
2454
3200
  if (resolvedOutputSchema) {
2455
3201
  resolvedOutputSchema = applyClientTarget(resolvedOutputSchema, options.target);
2456
3202
  }
2457
3203
  }
3204
+ const responseHints = detectResponseHints(resolvedOutputSchema, mapper);
3205
+ if (responseHints) {
3206
+ metadata.responseHints = responseHints;
3207
+ }
3208
+ let finalDescription = description;
3209
+ if (options.appendResponseSummary && resolvedOutputSchema) {
3210
+ const summary = summarizeOutputSchema(resolvedOutputSchema);
3211
+ if (summary) {
3212
+ finalDescription = `${finalDescription}
3213
+
3214
+ Returns: ${summary}`;
3215
+ }
3216
+ }
2458
3217
  return {
2459
3218
  name,
2460
3219
  ...title !== void 0 && { title },
2461
- description,
3220
+ description: finalDescription,
2462
3221
  ...annotations && { annotations },
2463
3222
  inputSchema: resolvedInputSchema,
2464
3223
  outputSchema: resolvedOutputSchema,
@@ -2534,7 +3293,9 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
2534
3293
  } else if (operationId) {
2535
3294
  rawName = operationId;
2536
3295
  } else {
2537
- const sanitized = path.replace(/\{([^}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
3296
+ const sanitized = trimUnderscores(
3297
+ path.replace(/\{([^{}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_")
3298
+ );
2538
3299
  rawName = `${method}_${sanitized}`;
2539
3300
  }
2540
3301
  return normalizeToolName(
@@ -2749,7 +3510,7 @@ var SecurityResolver = class {
2749
3510
  resolveDigestAuth(context) {
2750
3511
  const digest = context.digest;
2751
3512
  if (!digest) return void 0;
2752
- const quoted = (v) => String(v).replace(/[\r\n]/g, "").replace(/"/g, '\\"');
3513
+ const quoted = (v) => String(v).replace(/[\r\n]/g, "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2753
3514
  const token = (v) => String(v).replace(/[\r\n",]/g, "");
2754
3515
  const parts = [
2755
3516
  `username="${quoted(digest.username)}"`,
@@ -3210,6 +3971,44 @@ function toSdkTool(tool, wrapper) {
3210
3971
  }
3211
3972
  ];
3212
3973
  }
3974
+
3975
+ // src/token-report.ts
3976
+ function estimateToolTokens(tool) {
3977
+ const advertised = {
3978
+ name: tool.name,
3979
+ ...tool.title !== void 0 && { title: tool.title },
3980
+ description: tool.description,
3981
+ ...tool.annotations !== void 0 && { annotations: tool.annotations },
3982
+ inputSchema: tool.inputSchema,
3983
+ ...tool.outputSchema !== void 0 && { outputSchema: tool.outputSchema }
3984
+ };
3985
+ return Math.ceil(JSON.stringify(advertised).length / 4);
3986
+ }
3987
+ function analyzeToolSet(tools, options = {}) {
3988
+ const tokenBudget = options.tokenBudget ?? 1e4;
3989
+ const maxRecommendedTools = options.maxRecommendedTools ?? 40;
3990
+ const perToolWarning = options.perToolWarning ?? 2e3;
3991
+ const perTool = tools.map((tool) => ({ name: tool.name, tokens: estimateToolTokens(tool) })).sort((a, b) => b.tokens - a.tokens || (a.name < b.name ? -1 : 1));
3992
+ const estimatedTokens = perTool.reduce((sum, entry) => sum + entry.tokens, 0);
3993
+ const warnings = [];
3994
+ if (tools.length > maxRecommendedTools) {
3995
+ warnings.push(
3996
+ `${tools.length} tools exceeds the ~${maxRecommendedTools}-tool range where model selection accuracy degrades \u2014 curate with filters (tags, paths, readOnlyOnly) or split into focused servers.`
3997
+ );
3998
+ }
3999
+ if (estimatedTokens > tokenBudget) {
4000
+ warnings.push(
4001
+ `Estimated ${estimatedTokens} tokens of tool definitions exceeds the ${tokenBudget}-token budget \u2014 trim schemas (maxSchemaDepth, maxProperties) or reduce the tool count.`
4002
+ );
4003
+ }
4004
+ const heavy = perTool.filter((entry) => entry.tokens > perToolWarning);
4005
+ if (heavy.length > 0) {
4006
+ warnings.push(
4007
+ `${heavy.length} tool(s) exceed ${perToolWarning} tokens each (${heavy.slice(0, 3).map((entry) => `${entry.name}: ~${entry.tokens}`).join(", ")}${heavy.length > 3 ? ", \u2026" : ""}) \u2014 consider schema trimming for these.`
4008
+ );
4009
+ }
4010
+ return { toolCount: tools.length, estimatedTokens, perTool, warnings };
4011
+ }
3213
4012
  export {
3214
4013
  BLOCKED_HOSTNAMES,
3215
4014
  BUILTIN_FORMAT_RESOLVERS,
@@ -3217,6 +4016,7 @@ export {
3217
4016
  LoadError,
3218
4017
  OpenAPIToolError,
3219
4018
  OpenAPIToolGenerator,
4019
+ OverlayError,
3220
4020
  ParameterResolver,
3221
4021
  ParseError,
3222
4022
  RequestBuildError,
@@ -3227,7 +4027,9 @@ export {
3227
4027
  SsrfError,
3228
4028
  ValidationError,
3229
4029
  Validator,
4030
+ analyzeToolSet,
3230
4031
  applyClientTarget,
4032
+ applyOverlay,
3231
4033
  assertUrlSafe,
3232
4034
  buildHttpRequest,
3233
4035
  collapseNestedUnions,
@@ -3238,12 +4040,14 @@ export {
3238
4040
  demoteFormats,
3239
4041
  enforceClosedObjects,
3240
4042
  ensureArrayItems,
4043
+ estimateToolTokens,
3241
4044
  extractExtensionOverrides,
3242
4045
  inferAnnotationsFromMethod,
3243
4046
  inlineLocalRefs,
3244
4047
  isBlockedAddress,
3245
4048
  isBlockedHostname,
3246
4049
  isReferenceObject,
4050
+ lintDocument,
3247
4051
  normalizeSsrfOptions,
3248
4052
  requireAllProperties,
3249
4053
  resolveExtensionEnabled,