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/README.md +16 -1
- package/errors.d.ts +7 -0
- package/esm/index.mjs +864 -60
- package/esm/package.json +3 -3
- package/generator.d.ts +8 -0
- package/index.d.ts +8 -2
- package/index.js +869 -60
- package/lint.d.ts +33 -0
- package/overlay.d.ts +43 -0
- package/package.json +3 -3
- package/schema-builder.d.ts +17 -0
- package/token-report.d.ts +65 -0
- package/types.d.ts +63 -0
package/index.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
LoadError: () => LoadError,
|
|
37
37
|
OpenAPIToolError: () => OpenAPIToolError,
|
|
38
38
|
OpenAPIToolGenerator: () => OpenAPIToolGenerator,
|
|
39
|
+
OverlayError: () => OverlayError,
|
|
39
40
|
ParameterResolver: () => ParameterResolver,
|
|
40
41
|
ParseError: () => ParseError,
|
|
41
42
|
RequestBuildError: () => RequestBuildError,
|
|
@@ -46,7 +47,9 @@ __export(index_exports, {
|
|
|
46
47
|
SsrfError: () => SsrfError,
|
|
47
48
|
ValidationError: () => ValidationError,
|
|
48
49
|
Validator: () => Validator,
|
|
50
|
+
analyzeToolSet: () => analyzeToolSet,
|
|
49
51
|
applyClientTarget: () => applyClientTarget,
|
|
52
|
+
applyOverlay: () => applyOverlay,
|
|
50
53
|
assertUrlSafe: () => assertUrlSafe,
|
|
51
54
|
buildHttpRequest: () => buildHttpRequest,
|
|
52
55
|
collapseNestedUnions: () => collapseNestedUnions,
|
|
@@ -57,12 +60,14 @@ __export(index_exports, {
|
|
|
57
60
|
demoteFormats: () => demoteFormats,
|
|
58
61
|
enforceClosedObjects: () => enforceClosedObjects,
|
|
59
62
|
ensureArrayItems: () => ensureArrayItems,
|
|
63
|
+
estimateToolTokens: () => estimateToolTokens,
|
|
60
64
|
extractExtensionOverrides: () => extractExtensionOverrides,
|
|
61
65
|
inferAnnotationsFromMethod: () => inferAnnotationsFromMethod,
|
|
62
66
|
inlineLocalRefs: () => inlineLocalRefs,
|
|
63
67
|
isBlockedAddress: () => isBlockedAddress,
|
|
64
68
|
isBlockedHostname: () => isBlockedHostname,
|
|
65
69
|
isReferenceObject: () => isReferenceObject,
|
|
70
|
+
lintDocument: () => lintDocument,
|
|
66
71
|
normalizeSsrfOptions: () => normalizeSsrfOptions,
|
|
67
72
|
requireAllProperties: () => requireAllProperties,
|
|
68
73
|
resolveExtensionEnabled: () => resolveExtensionEnabled,
|
|
@@ -1005,6 +1010,96 @@ var SchemaBuilder = class {
|
|
|
1005
1010
|
}
|
|
1006
1011
|
return copy;
|
|
1007
1012
|
}
|
|
1013
|
+
// Copy-on-walk over every structural keyword (same key groups as
|
|
1014
|
+
// truncateDepth): `visit` transforms each node top-down and must return a
|
|
1015
|
+
// new node when it changes anything.
|
|
1016
|
+
static walkCopy(node, visit, seen = /* @__PURE__ */ new Map()) {
|
|
1017
|
+
if (!node || typeof node !== "object") return node;
|
|
1018
|
+
const existing = seen.get(node);
|
|
1019
|
+
if (existing) return existing;
|
|
1020
|
+
const copy = visit({ ...node });
|
|
1021
|
+
seen.set(node, copy);
|
|
1022
|
+
for (const key of this.TRUNCATE_MAP_KEYS) {
|
|
1023
|
+
const value = copy[key];
|
|
1024
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1025
|
+
const mapped = {};
|
|
1026
|
+
for (const [name, sub] of Object.entries(value)) {
|
|
1027
|
+
mapped[name] = this.walkCopy(sub, visit, seen);
|
|
1028
|
+
}
|
|
1029
|
+
copy[key] = mapped;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
for (const key of this.TRUNCATE_SCHEMA_KEYS) {
|
|
1033
|
+
const value = copy[key];
|
|
1034
|
+
if (Array.isArray(value)) {
|
|
1035
|
+
copy[key] = value.map((item) => this.walkCopy(item, visit, seen));
|
|
1036
|
+
} else if (value !== null && typeof value === "object") {
|
|
1037
|
+
copy[key] = this.walkCopy(value, visit, seen);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
for (const key of this.TRUNCATE_LIST_KEYS) {
|
|
1041
|
+
const value = copy[key];
|
|
1042
|
+
if (Array.isArray(value)) {
|
|
1043
|
+
copy[key] = value.map((member) => this.walkCopy(member, visit, seen));
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return copy;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Limit every object node to its first `max` properties (declaration
|
|
1050
|
+
* order). Dropped properties are pruned from `required` and counted in a
|
|
1051
|
+
* note appended to the node's description.
|
|
1052
|
+
*/
|
|
1053
|
+
static limitProperties(schema, max) {
|
|
1054
|
+
const bound = Number.isFinite(max) ? Math.max(1, Math.floor(max)) : Number.MAX_SAFE_INTEGER;
|
|
1055
|
+
return this.walkCopy(schema, (node) => {
|
|
1056
|
+
const properties = node.properties;
|
|
1057
|
+
if (!properties || typeof properties !== "object") return node;
|
|
1058
|
+
const entries = Object.entries(properties);
|
|
1059
|
+
if (entries.length <= bound) return node;
|
|
1060
|
+
const kept = entries.slice(0, bound);
|
|
1061
|
+
const keptNames = new Set(kept.map(([name]) => name));
|
|
1062
|
+
const dropped = entries.length - bound;
|
|
1063
|
+
const note = `[${dropped} additional propert${dropped === 1 ? "y" : "ies"} omitted: exceeds maxProperties]`;
|
|
1064
|
+
const next = { ...node, properties: Object.fromEntries(kept) };
|
|
1065
|
+
if (Array.isArray(node.required)) {
|
|
1066
|
+
const required = node.required.filter((name) => keptNames.has(String(name)));
|
|
1067
|
+
if (required.length > 0) {
|
|
1068
|
+
next.required = required;
|
|
1069
|
+
} else {
|
|
1070
|
+
delete next.required;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
next.description = node.description ? `${node.description} ${note}` : note;
|
|
1074
|
+
return next;
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Cap every description in the schema tree to `maxLength` characters,
|
|
1079
|
+
* truncating with an ellipsis.
|
|
1080
|
+
*/
|
|
1081
|
+
static capDescriptions(schema, maxLength) {
|
|
1082
|
+
const bound = Number.isFinite(maxLength) ? Math.max(1, Math.floor(maxLength)) : Number.MAX_SAFE_INTEGER;
|
|
1083
|
+
return this.walkCopy(schema, (node) => {
|
|
1084
|
+
if (typeof node.description === "string" && node.description.length > bound) {
|
|
1085
|
+
return { ...node, description: `${node.description.slice(0, bound - 1)}\u2026` };
|
|
1086
|
+
}
|
|
1087
|
+
return node;
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Remove every `examples` array from the schema tree (a token-budget
|
|
1092
|
+
* trimming step — validation keywords are untouched).
|
|
1093
|
+
*/
|
|
1094
|
+
static stripExamples(schema) {
|
|
1095
|
+
return this.walkCopy(schema, (node) => {
|
|
1096
|
+
if ("examples" in node) {
|
|
1097
|
+
const { examples: _examples, ...rest } = node;
|
|
1098
|
+
return rest;
|
|
1099
|
+
}
|
|
1100
|
+
return node;
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1008
1103
|
/**
|
|
1009
1104
|
* Simplify schema by removing unnecessary fields
|
|
1010
1105
|
*/
|
|
@@ -1445,6 +1540,557 @@ function applyClientTarget(schema, target) {
|
|
|
1445
1540
|
return result;
|
|
1446
1541
|
}
|
|
1447
1542
|
|
|
1543
|
+
// src/errors.ts
|
|
1544
|
+
var OpenAPIToolError = class extends Error {
|
|
1545
|
+
context;
|
|
1546
|
+
constructor(message, context) {
|
|
1547
|
+
super(message);
|
|
1548
|
+
this.name = this.constructor.name;
|
|
1549
|
+
this.context = context;
|
|
1550
|
+
if (Error.captureStackTrace) {
|
|
1551
|
+
Error.captureStackTrace(this, this.constructor);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
var LoadError = class extends OpenAPIToolError {
|
|
1556
|
+
constructor(message, context) {
|
|
1557
|
+
super(message, context);
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
var SsrfError = class extends LoadError {
|
|
1561
|
+
constructor(message, context) {
|
|
1562
|
+
super(message, context);
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
var ParseError = class extends OpenAPIToolError {
|
|
1566
|
+
constructor(message, context) {
|
|
1567
|
+
super(message, context);
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
var ValidationError = class extends OpenAPIToolError {
|
|
1571
|
+
errors;
|
|
1572
|
+
constructor(message, context) {
|
|
1573
|
+
super(message, context);
|
|
1574
|
+
this.errors = context?.["errors"];
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
var GenerationError = class extends OpenAPIToolError {
|
|
1578
|
+
constructor(message, context) {
|
|
1579
|
+
super(message, context);
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
var OverlayError = class extends OpenAPIToolError {
|
|
1583
|
+
constructor(message, context) {
|
|
1584
|
+
super(message, context);
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
var RequestBuildError = class extends OpenAPIToolError {
|
|
1588
|
+
constructor(message, context) {
|
|
1589
|
+
super(message, context);
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
var SchemaError = class extends OpenAPIToolError {
|
|
1593
|
+
constructor(message, context) {
|
|
1594
|
+
super(message, context);
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
// src/overlay.ts
|
|
1599
|
+
function parsePath(path) {
|
|
1600
|
+
if (typeof path !== "string" || !path.startsWith("$")) {
|
|
1601
|
+
throw new OverlayError(`Overlay target must be a JSONPath starting with '$'; received '${String(path)}'`, {
|
|
1602
|
+
target: path
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
const segments = [];
|
|
1606
|
+
let rest = path.slice(1);
|
|
1607
|
+
while (rest.length > 0) {
|
|
1608
|
+
let recursive = false;
|
|
1609
|
+
if (rest.startsWith("..")) {
|
|
1610
|
+
recursive = true;
|
|
1611
|
+
rest = rest.slice(2);
|
|
1612
|
+
const bare = rest.match(/^([A-Za-z_][\w-]*)/);
|
|
1613
|
+
if (bare) {
|
|
1614
|
+
segments.push({ kind: "child", name: bare[1], recursive });
|
|
1615
|
+
rest = rest.slice(bare[0].length);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
} else if (rest.startsWith(".")) {
|
|
1619
|
+
rest = rest.slice(1);
|
|
1620
|
+
if (rest.startsWith("*")) {
|
|
1621
|
+
segments.push({ kind: "wildcard", recursive });
|
|
1622
|
+
rest = rest.slice(1);
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
const bare = rest.match(/^([A-Za-z_][\w-]*)/);
|
|
1626
|
+
if (bare) {
|
|
1627
|
+
segments.push({ kind: "child", name: bare[1], recursive });
|
|
1628
|
+
rest = rest.slice(bare[0].length);
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
throw new OverlayError(`Invalid JSONPath segment after '.' in '${path}'`, { target: path });
|
|
1632
|
+
}
|
|
1633
|
+
if (!rest.startsWith("[")) {
|
|
1634
|
+
throw new OverlayError(`Invalid JSONPath segment at '${rest}' in '${path}'`, { target: path });
|
|
1635
|
+
}
|
|
1636
|
+
const bracket = matchBracket(rest, path);
|
|
1637
|
+
const inner = bracket.inner.trim();
|
|
1638
|
+
rest = bracket.rest;
|
|
1639
|
+
if (inner === "*") {
|
|
1640
|
+
segments.push({ kind: "wildcard", recursive });
|
|
1641
|
+
} else if (/^-?\d+$/.test(inner)) {
|
|
1642
|
+
segments.push({ kind: "index", index: parseInt(inner, 10), recursive });
|
|
1643
|
+
} else if (/^'.*'$/.test(inner) || /^".*"$/.test(inner)) {
|
|
1644
|
+
segments.push({ kind: "child", name: inner.slice(1, -1), recursive });
|
|
1645
|
+
} else if (inner.startsWith("?(") && inner.endsWith(")")) {
|
|
1646
|
+
segments.push(parseFilter(inner.slice(2, -1).trim(), path, recursive));
|
|
1647
|
+
} else {
|
|
1648
|
+
throw new OverlayError(`Unsupported JSONPath selector '[${inner}]' in '${path}'`, { target: path });
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return segments;
|
|
1652
|
+
}
|
|
1653
|
+
function matchBracket(input, fullPath) {
|
|
1654
|
+
let quote = null;
|
|
1655
|
+
let depth = 0;
|
|
1656
|
+
for (let i = 1; i < input.length; i++) {
|
|
1657
|
+
const char = input[i];
|
|
1658
|
+
if (quote) {
|
|
1659
|
+
if (char === quote) quote = null;
|
|
1660
|
+
} else if (char === "'" || char === '"') {
|
|
1661
|
+
quote = char;
|
|
1662
|
+
} else if (char === "[") {
|
|
1663
|
+
depth++;
|
|
1664
|
+
} else if (char === "]") {
|
|
1665
|
+
if (depth === 0) {
|
|
1666
|
+
return { inner: input.slice(1, i), rest: input.slice(i + 1) };
|
|
1667
|
+
}
|
|
1668
|
+
depth--;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
throw new OverlayError(`Unterminated '[' selector in '${fullPath}'`, { target: fullPath });
|
|
1672
|
+
}
|
|
1673
|
+
function parseFilter(expr, path, recursive) {
|
|
1674
|
+
const match = expr.match(/^@(?:\.([A-Za-z_][\w-]*)|\['([^']*)'\]|\["([^"]*)"\])\s*(?:(==|!=)\s*(.+))?$/);
|
|
1675
|
+
if (!match) {
|
|
1676
|
+
throw new OverlayError(`Unsupported filter expression '?(${expr})' in '${path}'`, { target: path });
|
|
1677
|
+
}
|
|
1678
|
+
const field = match[1] ?? match[2] ?? match[3];
|
|
1679
|
+
const op = match[4];
|
|
1680
|
+
if (!op) {
|
|
1681
|
+
return { kind: "filter", field, op: "exists", recursive };
|
|
1682
|
+
}
|
|
1683
|
+
const raw = match[5].trim();
|
|
1684
|
+
let literal;
|
|
1685
|
+
if (/^'.*'$/.test(raw) || /^".*"$/.test(raw)) {
|
|
1686
|
+
literal = raw.slice(1, -1);
|
|
1687
|
+
} else if (/^-?\d+(\.\d+)?$/.test(raw)) {
|
|
1688
|
+
literal = parseFloat(raw);
|
|
1689
|
+
} else if (raw === "true" || raw === "false") {
|
|
1690
|
+
literal = raw === "true";
|
|
1691
|
+
} else {
|
|
1692
|
+
throw new OverlayError(`Unsupported filter literal '${raw}' in '${path}'`, { target: path });
|
|
1693
|
+
}
|
|
1694
|
+
return { kind: "filter", field, op, literal, recursive };
|
|
1695
|
+
}
|
|
1696
|
+
function isContainer(value) {
|
|
1697
|
+
return value !== null && typeof value === "object";
|
|
1698
|
+
}
|
|
1699
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1700
|
+
function descendants(match) {
|
|
1701
|
+
const result = [];
|
|
1702
|
+
const walk = (node) => {
|
|
1703
|
+
if (!isContainer(node)) return;
|
|
1704
|
+
if (Array.isArray(node)) {
|
|
1705
|
+
node.forEach((item, index) => {
|
|
1706
|
+
result.push({ parent: node, key: index, value: item });
|
|
1707
|
+
walk(item);
|
|
1708
|
+
});
|
|
1709
|
+
} else {
|
|
1710
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1711
|
+
result.push({ parent: node, key, value });
|
|
1712
|
+
walk(value);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
walk(match.value);
|
|
1717
|
+
return result;
|
|
1718
|
+
}
|
|
1719
|
+
function dedupeMatches(matches) {
|
|
1720
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1721
|
+
const result = [];
|
|
1722
|
+
for (const match of matches) {
|
|
1723
|
+
let keys = seen.get(match.parent);
|
|
1724
|
+
if (!keys) {
|
|
1725
|
+
keys = /* @__PURE__ */ new Set();
|
|
1726
|
+
seen.set(match.parent, keys);
|
|
1727
|
+
}
|
|
1728
|
+
if (keys.has(match.key)) continue;
|
|
1729
|
+
keys.add(match.key);
|
|
1730
|
+
result.push(match);
|
|
1731
|
+
}
|
|
1732
|
+
return result;
|
|
1733
|
+
}
|
|
1734
|
+
function applySegment(matches, segment) {
|
|
1735
|
+
const scope = segment.recursive ? matches.flatMap((m) => [m, ...descendants(m)]) : matches;
|
|
1736
|
+
const next = [];
|
|
1737
|
+
for (const match of scope) {
|
|
1738
|
+
const node = match.value;
|
|
1739
|
+
switch (segment.kind) {
|
|
1740
|
+
case "child": {
|
|
1741
|
+
if (isContainer(node) && !Array.isArray(node) && !UNSAFE_KEYS.has(segment.name) && Object.prototype.hasOwnProperty.call(node, segment.name)) {
|
|
1742
|
+
next.push({ parent: node, key: segment.name, value: node[segment.name] });
|
|
1743
|
+
}
|
|
1744
|
+
break;
|
|
1745
|
+
}
|
|
1746
|
+
case "wildcard": {
|
|
1747
|
+
if (Array.isArray(node)) {
|
|
1748
|
+
node.forEach((item, index) => next.push({ parent: node, key: index, value: item }));
|
|
1749
|
+
} else if (isContainer(node)) {
|
|
1750
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1751
|
+
next.push({ parent: node, key, value });
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
break;
|
|
1755
|
+
}
|
|
1756
|
+
case "index": {
|
|
1757
|
+
if (Array.isArray(node)) {
|
|
1758
|
+
const index = segment.index < 0 ? node.length + segment.index : segment.index;
|
|
1759
|
+
if (index >= 0 && index < node.length) {
|
|
1760
|
+
next.push({ parent: node, key: index, value: node[index] });
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1765
|
+
case "filter": {
|
|
1766
|
+
const members = Array.isArray(node) ? node.map((item, index) => ({ parent: node, key: index, value: item })) : isContainer(node) ? Object.entries(node).map(([key, value]) => ({ parent: node, key, value })) : [];
|
|
1767
|
+
for (const member of members) {
|
|
1768
|
+
if (!isContainer(member.value) || Array.isArray(member.value)) continue;
|
|
1769
|
+
const fieldValue = member.value[segment.field];
|
|
1770
|
+
const keep = segment.op === "exists" ? fieldValue !== void 0 : segment.op === "==" ? fieldValue === segment.literal : fieldValue !== segment.literal;
|
|
1771
|
+
if (keep) next.push(member);
|
|
1772
|
+
}
|
|
1773
|
+
break;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return next;
|
|
1778
|
+
}
|
|
1779
|
+
function deepMerge(target, update) {
|
|
1780
|
+
for (const [key, value] of Object.entries(update)) {
|
|
1781
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
1782
|
+
const existing = target[key];
|
|
1783
|
+
if (isContainer(value) && !Array.isArray(value) && isContainer(existing) && !Array.isArray(existing)) {
|
|
1784
|
+
deepMerge(existing, value);
|
|
1785
|
+
} else {
|
|
1786
|
+
target[key] = value;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
function applyOverlay(document, overlay) {
|
|
1791
|
+
if (!overlay || typeof overlay !== "object" || !Array.isArray(overlay.actions)) {
|
|
1792
|
+
throw new OverlayError("Overlay document must have an actions array", {});
|
|
1793
|
+
}
|
|
1794
|
+
const result = JSON.parse(JSON.stringify(document));
|
|
1795
|
+
for (const [index, action] of overlay.actions.entries()) {
|
|
1796
|
+
if (!action || typeof action !== "object" || typeof action.target !== "string") {
|
|
1797
|
+
throw new OverlayError(`Overlay action #${index} must have a string target`, { index });
|
|
1798
|
+
}
|
|
1799
|
+
if (action.update === void 0 && action.remove !== true) {
|
|
1800
|
+
throw new OverlayError(`Overlay action #${index} needs 'update' or 'remove: true'`, {
|
|
1801
|
+
index,
|
|
1802
|
+
target: action.target
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
const segments = parsePath(action.target);
|
|
1806
|
+
let matches = [{ parent: null, key: null, value: result }];
|
|
1807
|
+
for (const segment of segments) {
|
|
1808
|
+
matches = dedupeMatches(applySegment(matches, segment));
|
|
1809
|
+
}
|
|
1810
|
+
if (action.remove === true) {
|
|
1811
|
+
const arrayRemovals = /* @__PURE__ */ new Map();
|
|
1812
|
+
for (const match of matches) {
|
|
1813
|
+
if (match.parent === null) {
|
|
1814
|
+
throw new OverlayError("Overlay cannot remove the document root", { target: action.target });
|
|
1815
|
+
}
|
|
1816
|
+
if (Array.isArray(match.parent)) {
|
|
1817
|
+
const indices = arrayRemovals.get(match.parent) ?? [];
|
|
1818
|
+
indices.push(match.key);
|
|
1819
|
+
arrayRemovals.set(match.parent, indices);
|
|
1820
|
+
} else {
|
|
1821
|
+
delete match.parent[match.key];
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
for (const [parent, indices] of arrayRemovals) {
|
|
1825
|
+
for (const index2 of indices.sort((a, b) => b - a)) {
|
|
1826
|
+
parent.splice(index2, 1);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
for (const match of matches) {
|
|
1832
|
+
const node = match.value;
|
|
1833
|
+
if (Array.isArray(node)) {
|
|
1834
|
+
node.push(action.update);
|
|
1835
|
+
} else if (isContainer(node) && isContainer(action.update) && !Array.isArray(action.update)) {
|
|
1836
|
+
deepMerge(node, action.update);
|
|
1837
|
+
} else {
|
|
1838
|
+
if (match.parent === null) {
|
|
1839
|
+
throw new OverlayError("Overlay cannot replace the document root with a non-object", {
|
|
1840
|
+
target: action.target
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
match.parent[match.key] = action.update;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
return result;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// src/lint.ts
|
|
1851
|
+
var METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
|
|
1852
|
+
var PAGINATION_PARAM = /^(page|limit|offset|cursor|per_page|pagesize|page_size|after|before)$/i;
|
|
1853
|
+
var DEEP_SCHEMA_THRESHOLD = 8;
|
|
1854
|
+
var WIDE_SCHEMA_THRESHOLD = 30;
|
|
1855
|
+
function measureSchema(node, seen = /* @__PURE__ */ new Map()) {
|
|
1856
|
+
if (node === null || typeof node !== "object") {
|
|
1857
|
+
return { depth: 0, widestObject: 0, hasArray: false };
|
|
1858
|
+
}
|
|
1859
|
+
if (seen.has(node)) {
|
|
1860
|
+
return seen.get(node) ?? { depth: 0, widestObject: 0, hasArray: false };
|
|
1861
|
+
}
|
|
1862
|
+
seen.set(node, null);
|
|
1863
|
+
const record = node;
|
|
1864
|
+
let childDepth = 0;
|
|
1865
|
+
let widestObject = 0;
|
|
1866
|
+
let hasArray = record["type"] === "array" || Array.isArray(record["type"]) && record["type"].includes("array");
|
|
1867
|
+
const visit = (child) => {
|
|
1868
|
+
const shape2 = measureSchema(child, seen);
|
|
1869
|
+
childDepth = Math.max(childDepth, shape2.depth);
|
|
1870
|
+
widestObject = Math.max(widestObject, shape2.widestObject);
|
|
1871
|
+
hasArray = hasArray || shape2.hasArray;
|
|
1872
|
+
};
|
|
1873
|
+
const properties = record["properties"];
|
|
1874
|
+
if (properties && typeof properties === "object") {
|
|
1875
|
+
widestObject = Math.max(widestObject, Object.keys(properties).length);
|
|
1876
|
+
for (const child of Object.values(properties)) visit(child);
|
|
1877
|
+
}
|
|
1878
|
+
for (const key of ["items", "additionalProperties", "not", "contentSchema"]) {
|
|
1879
|
+
const value = record[key];
|
|
1880
|
+
if (value && typeof value === "object" && !Array.isArray(value)) visit(value);
|
|
1881
|
+
if (Array.isArray(value)) value.forEach(visit);
|
|
1882
|
+
}
|
|
1883
|
+
for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
|
|
1884
|
+
const value = record[key];
|
|
1885
|
+
if (Array.isArray(value)) value.forEach(visit);
|
|
1886
|
+
}
|
|
1887
|
+
const shape = { depth: childDepth + 1, widestObject, hasArray };
|
|
1888
|
+
seen.set(node, shape);
|
|
1889
|
+
return shape;
|
|
1890
|
+
}
|
|
1891
|
+
function schemaHasExample(node, seen = /* @__PURE__ */ new Set()) {
|
|
1892
|
+
if (node === null || typeof node !== "object" || seen.has(node)) return false;
|
|
1893
|
+
seen.add(node);
|
|
1894
|
+
const record = node;
|
|
1895
|
+
if (record["example"] !== void 0 || record["examples"] !== void 0) return true;
|
|
1896
|
+
const properties = record["properties"];
|
|
1897
|
+
if (properties && typeof properties === "object") {
|
|
1898
|
+
if (Object.values(properties).some((child) => schemaHasExample(child, seen))) return true;
|
|
1899
|
+
}
|
|
1900
|
+
for (const key of ["items", "additionalProperties", "not", "contentSchema"]) {
|
|
1901
|
+
const value = record[key];
|
|
1902
|
+
if (value && typeof value === "object" && !Array.isArray(value) && schemaHasExample(value, seen)) return true;
|
|
1903
|
+
if (Array.isArray(value) && value.some((item) => schemaHasExample(item, seen))) return true;
|
|
1904
|
+
}
|
|
1905
|
+
for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
|
|
1906
|
+
const value = record[key];
|
|
1907
|
+
if (Array.isArray(value) && value.some((member) => schemaHasExample(member, seen))) return true;
|
|
1908
|
+
}
|
|
1909
|
+
return false;
|
|
1910
|
+
}
|
|
1911
|
+
function hasAnyExample(content) {
|
|
1912
|
+
if (!content) return false;
|
|
1913
|
+
return Object.values(content).some((media) => {
|
|
1914
|
+
if (!media || typeof media !== "object") return false;
|
|
1915
|
+
const record = media;
|
|
1916
|
+
if (record["example"] !== void 0 || record["examples"] !== void 0) return true;
|
|
1917
|
+
return schemaHasExample(record["schema"]);
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
function lintDocument(document) {
|
|
1921
|
+
const findings = [];
|
|
1922
|
+
const operationIds = /* @__PURE__ */ new Map();
|
|
1923
|
+
const paths = document.paths ?? {};
|
|
1924
|
+
for (const [pathStr, pathItem] of Object.entries(paths).sort(([a], [b]) => a < b ? -1 : 1)) {
|
|
1925
|
+
if (!pathItem || "$ref" in pathItem) continue;
|
|
1926
|
+
const pathLevelParameters = (pathItem["parameters"] ?? []).filter(
|
|
1927
|
+
(param) => !isReferenceObject(param)
|
|
1928
|
+
);
|
|
1929
|
+
for (const method of METHODS) {
|
|
1930
|
+
const operation = pathItem[method];
|
|
1931
|
+
if (!operation) continue;
|
|
1932
|
+
const label = `${method.toUpperCase()} ${pathStr}`;
|
|
1933
|
+
if (!operation.operationId) {
|
|
1934
|
+
findings.push({
|
|
1935
|
+
severity: "warning",
|
|
1936
|
+
code: "missing-operation-id",
|
|
1937
|
+
message: "Operation has no operationId; the tool name will be generated from the method and path.",
|
|
1938
|
+
path: label,
|
|
1939
|
+
hint: "Add a short, action-oriented operationId (it becomes the tool name)."
|
|
1940
|
+
});
|
|
1941
|
+
} else {
|
|
1942
|
+
const existing = operationIds.get(operation.operationId) ?? [];
|
|
1943
|
+
existing.push(label);
|
|
1944
|
+
operationIds.set(operation.operationId, existing);
|
|
1945
|
+
if (operation.operationId.length > 64) {
|
|
1946
|
+
findings.push({
|
|
1947
|
+
severity: "info",
|
|
1948
|
+
code: "long-operation-id",
|
|
1949
|
+
message: `operationId '${operation.operationId.slice(0, 40)}\u2026' exceeds 64 characters and will be truncated with a hash suffix.`,
|
|
1950
|
+
path: label,
|
|
1951
|
+
hint: "Shorten the operationId below 64 characters to keep tool names readable."
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
const prose = `${operation.summary ?? ""} ${operation.description ?? ""}`.trim();
|
|
1956
|
+
if (prose.length === 0) {
|
|
1957
|
+
findings.push({
|
|
1958
|
+
severity: "warning",
|
|
1959
|
+
code: "missing-description",
|
|
1960
|
+
message: "Operation has neither summary nor description; the model only sees the method and path.",
|
|
1961
|
+
path: label,
|
|
1962
|
+
hint: "Describe WHEN to use this operation and what it returns (or patch it in with an overlay)."
|
|
1963
|
+
});
|
|
1964
|
+
} else if (prose.length < 20) {
|
|
1965
|
+
findings.push({
|
|
1966
|
+
severity: "info",
|
|
1967
|
+
code: "vague-description",
|
|
1968
|
+
message: `Operation description is only ${prose.length} characters \u2014 likely too vague for reliable tool selection.`,
|
|
1969
|
+
path: label,
|
|
1970
|
+
hint: "Expand the description with the use case and key parameters."
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
const parameters = [
|
|
1974
|
+
...pathLevelParameters,
|
|
1975
|
+
...(operation.parameters ?? []).filter((param) => !isReferenceObject(param))
|
|
1976
|
+
];
|
|
1977
|
+
const undescribed = parameters.filter((param) => !param.description).map((param) => param.name);
|
|
1978
|
+
if (undescribed.length > 0) {
|
|
1979
|
+
findings.push({
|
|
1980
|
+
severity: "info",
|
|
1981
|
+
code: "missing-parameter-description",
|
|
1982
|
+
message: `Parameter(s) without description: ${undescribed.join(", ")}.`,
|
|
1983
|
+
path: label,
|
|
1984
|
+
hint: "Describe each parameter \u2014 models mis-fill undocumented arguments."
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
const responses = operation.responses ?? {};
|
|
1988
|
+
const successCodes = Object.keys(responses).filter((code) => /^2(\d\d|XX)$/i.test(code));
|
|
1989
|
+
if (successCodes.length === 0 && !responses["default"]) {
|
|
1990
|
+
findings.push({
|
|
1991
|
+
severity: "warning",
|
|
1992
|
+
code: "missing-success-response",
|
|
1993
|
+
message: "Operation declares no 2xx or default response; no output schema can be generated.",
|
|
1994
|
+
path: label,
|
|
1995
|
+
hint: "Add the success response with its schema."
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
let responseShape = { depth: 0, widestObject: 0, hasArray: false };
|
|
1999
|
+
for (const code of [...successCodes, "default"]) {
|
|
2000
|
+
const response = responses[code];
|
|
2001
|
+
if (!response || typeof response !== "object" || isReferenceObject(response)) continue;
|
|
2002
|
+
const content = response["content"];
|
|
2003
|
+
if (!content) continue;
|
|
2004
|
+
for (const media of Object.values(content)) {
|
|
2005
|
+
const schema = media && typeof media === "object" ? media["schema"] : void 0;
|
|
2006
|
+
const shape = measureSchema(schema);
|
|
2007
|
+
responseShape = {
|
|
2008
|
+
depth: Math.max(responseShape.depth, shape.depth),
|
|
2009
|
+
widestObject: Math.max(responseShape.widestObject, shape.widestObject),
|
|
2010
|
+
hasArray: responseShape.hasArray || shape.hasArray
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (method === "get" && responseShape.hasArray) {
|
|
2015
|
+
const hasPagination = parameters.some((param) => param.in === "query" && PAGINATION_PARAM.test(param.name));
|
|
2016
|
+
if (!hasPagination) {
|
|
2017
|
+
findings.push({
|
|
2018
|
+
severity: "warning",
|
|
2019
|
+
code: "unpaginated-list",
|
|
2020
|
+
message: "GET returns an array but declares no pagination parameter \u2014 responses can blow past client result limits (Claude Code caps tool results at 25K tokens).",
|
|
2021
|
+
path: label,
|
|
2022
|
+
hint: "Add limit/cursor/page parameters, or shape responses at the server."
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
const body = operation.requestBody;
|
|
2027
|
+
const bodyContent = body && !isReferenceObject(body) ? body.content : void 0;
|
|
2028
|
+
let requestShape = { depth: 0, widestObject: 0, hasArray: false };
|
|
2029
|
+
for (const media of Object.values(bodyContent ?? {})) {
|
|
2030
|
+
const schema = media && typeof media === "object" ? media["schema"] : void 0;
|
|
2031
|
+
const shape = measureSchema(schema);
|
|
2032
|
+
requestShape = {
|
|
2033
|
+
depth: Math.max(requestShape.depth, shape.depth),
|
|
2034
|
+
widestObject: Math.max(requestShape.widestObject, shape.widestObject),
|
|
2035
|
+
hasArray: requestShape.hasArray || shape.hasArray
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
const maxDepth = Math.max(requestShape.depth, responseShape.depth);
|
|
2039
|
+
if (maxDepth > DEEP_SCHEMA_THRESHOLD) {
|
|
2040
|
+
findings.push({
|
|
2041
|
+
severity: "warning",
|
|
2042
|
+
code: "deep-schema",
|
|
2043
|
+
message: `Schema nesting reaches depth ${maxDepth} (threshold ${DEEP_SCHEMA_THRESHOLD}) \u2014 deep schemas cost tokens and reduce accuracy.`,
|
|
2044
|
+
path: label,
|
|
2045
|
+
hint: "Flatten the schema, or bound generation with maxSchemaDepth."
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
const maxWidth = Math.max(requestShape.widestObject, responseShape.widestObject);
|
|
2049
|
+
if (maxWidth > WIDE_SCHEMA_THRESHOLD) {
|
|
2050
|
+
findings.push({
|
|
2051
|
+
severity: "info",
|
|
2052
|
+
code: "wide-schema",
|
|
2053
|
+
message: `An object schema declares ${maxWidth} properties (threshold ${WIDE_SCHEMA_THRESHOLD}).`,
|
|
2054
|
+
path: label,
|
|
2055
|
+
hint: "Split the payload, or bound generation with maxProperties."
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
if (bodyContent && !hasAnyExample(bodyContent)) {
|
|
2059
|
+
findings.push({
|
|
2060
|
+
severity: "info",
|
|
2061
|
+
code: "missing-request-example",
|
|
2062
|
+
message: "Request body has no example \u2014 examples measurably improve complex-parameter accuracy.",
|
|
2063
|
+
path: label,
|
|
2064
|
+
hint: "Add a media-type example (and enable includeExamples), or patch one in with an overlay."
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
for (const [operationId, labels] of operationIds) {
|
|
2070
|
+
if (labels.length > 1) {
|
|
2071
|
+
findings.push({
|
|
2072
|
+
severity: "error",
|
|
2073
|
+
code: "duplicate-operation-id",
|
|
2074
|
+
message: `operationId '${operationId}' is used by ${labels.length} operations: ${labels.join(", ")}.`,
|
|
2075
|
+
path: labels[0],
|
|
2076
|
+
hint: "Make operationIds unique \u2014 duplicates force hash-suffixed tool names."
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
const rank = { error: 0, warning: 1, info: 2 };
|
|
2081
|
+
findings.sort(
|
|
2082
|
+
(a, b) => rank[a.severity] - rank[b.severity] || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0) || (a.code < b.code ? -1 : 1)
|
|
2083
|
+
);
|
|
2084
|
+
return {
|
|
2085
|
+
findings,
|
|
2086
|
+
counts: {
|
|
2087
|
+
error: findings.filter((f) => f.severity === "error").length,
|
|
2088
|
+
warning: findings.filter((f) => f.severity === "warning").length,
|
|
2089
|
+
info: findings.filter((f) => f.severity === "info").length
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
|
|
1448
2094
|
// src/validator.ts
|
|
1449
2095
|
var Validator = class {
|
|
1450
2096
|
/**
|
|
@@ -1576,7 +2222,7 @@ var Validator = class {
|
|
|
1576
2222
|
if (operation.parameters) {
|
|
1577
2223
|
this.validateParameters(operation.parameters, path, method, errors, warnings);
|
|
1578
2224
|
}
|
|
1579
|
-
const pathParams = path.match(/\{([^}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
|
|
2225
|
+
const pathParams = path.match(/\{([^{}]+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
|
|
1580
2226
|
const definedPathParams = new Set(
|
|
1581
2227
|
operation.parameters?.filter((p) => p.in === "path").map((p) => p.name) ?? []
|
|
1582
2228
|
);
|
|
@@ -1636,56 +2282,6 @@ var Validator = class {
|
|
|
1636
2282
|
}
|
|
1637
2283
|
};
|
|
1638
2284
|
|
|
1639
|
-
// src/errors.ts
|
|
1640
|
-
var OpenAPIToolError = class extends Error {
|
|
1641
|
-
context;
|
|
1642
|
-
constructor(message, context) {
|
|
1643
|
-
super(message);
|
|
1644
|
-
this.name = this.constructor.name;
|
|
1645
|
-
this.context = context;
|
|
1646
|
-
if (Error.captureStackTrace) {
|
|
1647
|
-
Error.captureStackTrace(this, this.constructor);
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
};
|
|
1651
|
-
var LoadError = class extends OpenAPIToolError {
|
|
1652
|
-
constructor(message, context) {
|
|
1653
|
-
super(message, context);
|
|
1654
|
-
}
|
|
1655
|
-
};
|
|
1656
|
-
var SsrfError = class extends LoadError {
|
|
1657
|
-
constructor(message, context) {
|
|
1658
|
-
super(message, context);
|
|
1659
|
-
}
|
|
1660
|
-
};
|
|
1661
|
-
var ParseError = class extends OpenAPIToolError {
|
|
1662
|
-
constructor(message, context) {
|
|
1663
|
-
super(message, context);
|
|
1664
|
-
}
|
|
1665
|
-
};
|
|
1666
|
-
var ValidationError = class extends OpenAPIToolError {
|
|
1667
|
-
errors;
|
|
1668
|
-
constructor(message, context) {
|
|
1669
|
-
super(message, context);
|
|
1670
|
-
this.errors = context?.["errors"];
|
|
1671
|
-
}
|
|
1672
|
-
};
|
|
1673
|
-
var GenerationError = class extends OpenAPIToolError {
|
|
1674
|
-
constructor(message, context) {
|
|
1675
|
-
super(message, context);
|
|
1676
|
-
}
|
|
1677
|
-
};
|
|
1678
|
-
var RequestBuildError = class extends OpenAPIToolError {
|
|
1679
|
-
constructor(message, context) {
|
|
1680
|
-
super(message, context);
|
|
1681
|
-
}
|
|
1682
|
-
};
|
|
1683
|
-
var SchemaError = class extends OpenAPIToolError {
|
|
1684
|
-
constructor(message, context) {
|
|
1685
|
-
super(message, context);
|
|
1686
|
-
}
|
|
1687
|
-
};
|
|
1688
|
-
|
|
1689
2285
|
// src/format-resolver.ts
|
|
1690
2286
|
var BUILTIN_FORMAT_RESOLVERS = {
|
|
1691
2287
|
// String formats
|
|
@@ -2084,6 +2680,103 @@ function applySecureDefaults(options) {
|
|
|
2084
2680
|
}
|
|
2085
2681
|
};
|
|
2086
2682
|
}
|
|
2683
|
+
function hasUnboundedArray(node, seen = /* @__PURE__ */ new Set()) {
|
|
2684
|
+
if (node === null || typeof node !== "object" || seen.has(node)) return false;
|
|
2685
|
+
seen.add(node);
|
|
2686
|
+
const record = node;
|
|
2687
|
+
const type = record["type"];
|
|
2688
|
+
const isArray = type === "array" || Array.isArray(type) && type.includes("array");
|
|
2689
|
+
if (isArray && record["maxItems"] === void 0) return true;
|
|
2690
|
+
const children = [];
|
|
2691
|
+
const properties = record["properties"];
|
|
2692
|
+
if (properties && typeof properties === "object") children.push(...Object.values(properties));
|
|
2693
|
+
for (const key of ["items", "additionalProperties", "contentSchema"]) {
|
|
2694
|
+
const value = record[key];
|
|
2695
|
+
if (Array.isArray(value)) children.push(...value);
|
|
2696
|
+
else if (value && typeof value === "object") children.push(value);
|
|
2697
|
+
}
|
|
2698
|
+
for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
|
|
2699
|
+
if (Array.isArray(record[key])) children.push(...record[key]);
|
|
2700
|
+
}
|
|
2701
|
+
return children.some((child) => hasUnboundedArray(child, seen));
|
|
2702
|
+
}
|
|
2703
|
+
function detectResponseHints(outputSchema, mapper) {
|
|
2704
|
+
const paginationParams = [
|
|
2705
|
+
...new Set(mapper.filter((m) => m.type === "query" && !m.security && PAGINATION_PARAM.test(m.key)).map((m) => m.key))
|
|
2706
|
+
];
|
|
2707
|
+
const unboundedArray = outputSchema !== void 0 && hasUnboundedArray(outputSchema);
|
|
2708
|
+
if (!unboundedArray && paginationParams.length === 0) return void 0;
|
|
2709
|
+
return {
|
|
2710
|
+
...unboundedArray && { unboundedArray: true },
|
|
2711
|
+
...paginationParams.length > 0 && { paginationParams },
|
|
2712
|
+
...unboundedArray && paginationParams.length === 0 && { largeResponseRisk: true }
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
function composeDescription(operation, method, pathStr, strategy) {
|
|
2716
|
+
const fallback = `${method.toUpperCase()} ${pathStr}`;
|
|
2717
|
+
const summary = operation.summary?.trim();
|
|
2718
|
+
const description = operation.description?.trim();
|
|
2719
|
+
switch (strategy) {
|
|
2720
|
+
case "descriptionOnly":
|
|
2721
|
+
return description || summary || fallback;
|
|
2722
|
+
case "combined":
|
|
2723
|
+
if (summary && description && summary !== description) {
|
|
2724
|
+
return `${summary}
|
|
2725
|
+
|
|
2726
|
+
${description}`;
|
|
2727
|
+
}
|
|
2728
|
+
return summary || description || fallback;
|
|
2729
|
+
case "full": {
|
|
2730
|
+
const parts = [];
|
|
2731
|
+
if (summary) parts.push(summary);
|
|
2732
|
+
if (description && description !== summary) parts.push(description);
|
|
2733
|
+
if (operation.operationId) parts.push(`Operation: ${operation.operationId}`);
|
|
2734
|
+
parts.push(fallback);
|
|
2735
|
+
return parts.join("\n\n");
|
|
2736
|
+
}
|
|
2737
|
+
default:
|
|
2738
|
+
return summary || description || fallback;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
function propertyNames(schema, cap = 8) {
|
|
2742
|
+
const properties = schema["properties"];
|
|
2743
|
+
if (!properties || typeof properties !== "object") return "";
|
|
2744
|
+
const names = Object.keys(properties);
|
|
2745
|
+
const listed = names.slice(0, cap).join(", ");
|
|
2746
|
+
return names.length > cap ? `${listed}, \u2026` : listed;
|
|
2747
|
+
}
|
|
2748
|
+
function summarizeOutputSchema(schema) {
|
|
2749
|
+
const record = schema;
|
|
2750
|
+
const variants = record["oneOf"];
|
|
2751
|
+
if (Array.isArray(variants) && variants.length > 0) {
|
|
2752
|
+
const first = variants[0];
|
|
2753
|
+
const firstSummary = first && typeof first === "object" ? summarizeOutputSchema(first) : void 0;
|
|
2754
|
+
return firstSummary ? `${firstSummary} (${variants.length} response variants)` : void 0;
|
|
2755
|
+
}
|
|
2756
|
+
const type = record["type"];
|
|
2757
|
+
if (type === "object" || type === void 0 && record["properties"]) {
|
|
2758
|
+
const names = propertyNames(record);
|
|
2759
|
+
return names ? `object with fields: ${names}` : "object";
|
|
2760
|
+
}
|
|
2761
|
+
if (type === "array") {
|
|
2762
|
+
const items = record["items"];
|
|
2763
|
+
if (items && typeof items === "object" && !Array.isArray(items)) {
|
|
2764
|
+
const itemRecord = items;
|
|
2765
|
+
if (itemRecord["type"] === "object" || itemRecord["properties"]) {
|
|
2766
|
+
const names = propertyNames(itemRecord);
|
|
2767
|
+
return names ? `array of objects with fields: ${names}` : "array of objects";
|
|
2768
|
+
}
|
|
2769
|
+
if (typeof itemRecord["type"] === "string") {
|
|
2770
|
+
return `array of ${itemRecord["type"]}`;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
return "array";
|
|
2774
|
+
}
|
|
2775
|
+
if (typeof type === "string" && type !== "null") {
|
|
2776
|
+
return type;
|
|
2777
|
+
}
|
|
2778
|
+
return void 0;
|
|
2779
|
+
}
|
|
2087
2780
|
function globToRegExp(glob) {
|
|
2088
2781
|
let pattern = "^";
|
|
2089
2782
|
for (let i = 0; i < glob.length; i++) {
|
|
@@ -2106,6 +2799,13 @@ function globToRegExp(glob) {
|
|
|
2106
2799
|
function matchesAnyGlob(path, globs) {
|
|
2107
2800
|
return globs.some((glob) => globToRegExp(glob).test(path));
|
|
2108
2801
|
}
|
|
2802
|
+
function trimUnderscores(value) {
|
|
2803
|
+
let start = 0;
|
|
2804
|
+
let end = value.length;
|
|
2805
|
+
while (start < end && value[start] === "_") start++;
|
|
2806
|
+
while (end > start && value[end - 1] === "_") end--;
|
|
2807
|
+
return value.slice(start, end);
|
|
2808
|
+
}
|
|
2109
2809
|
function fnv1aHex(input) {
|
|
2110
2810
|
let hash = 2166136261;
|
|
2111
2811
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -2116,7 +2816,7 @@ function fnv1aHex(input) {
|
|
|
2116
2816
|
}
|
|
2117
2817
|
function normalizeToolName(raw, maxLength, fallbackSeed) {
|
|
2118
2818
|
let hashSeed = raw;
|
|
2119
|
-
let name = raw.replace(/[^A-Za-z0-9_.-]/g, "_").replace(/_+/g, "_")
|
|
2819
|
+
let name = trimUnderscores(raw.replace(/[^A-Za-z0-9_.-]/g, "_").replace(/_+/g, "_"));
|
|
2120
2820
|
if (name.length === 0) {
|
|
2121
2821
|
hashSeed = fallbackSeed;
|
|
2122
2822
|
name = `tool_${fnv1aHex(fallbackSeed)}`;
|
|
@@ -2149,8 +2849,15 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
|
2149
2849
|
validate: options.validate ?? true,
|
|
2150
2850
|
followRedirects: options.followRedirects ?? true,
|
|
2151
2851
|
refResolution: options.refResolution ?? {},
|
|
2152
|
-
secureDefaults: options.secureDefaults ?? false
|
|
2852
|
+
secureDefaults: options.secureDefaults ?? false,
|
|
2853
|
+
overlays: options.overlays
|
|
2153
2854
|
};
|
|
2855
|
+
if (this.options.overlays) {
|
|
2856
|
+
const overlays = Array.isArray(this.options.overlays) ? this.options.overlays : [this.options.overlays];
|
|
2857
|
+
for (const overlay of overlays) {
|
|
2858
|
+
this.document = applyOverlay(this.document, overlay);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2154
2861
|
}
|
|
2155
2862
|
/**
|
|
2156
2863
|
* Create generator from a URL
|
|
@@ -2180,7 +2887,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
|
2180
2887
|
}
|
|
2181
2888
|
return new _OpenAPIToolGenerator(document, options);
|
|
2182
2889
|
} catch (error) {
|
|
2183
|
-
if (error instanceof LoadError) {
|
|
2890
|
+
if (error instanceof LoadError || error instanceof OverlayError) {
|
|
2184
2891
|
throw error;
|
|
2185
2892
|
}
|
|
2186
2893
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -2213,6 +2920,9 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
|
2213
2920
|
}
|
|
2214
2921
|
return new _OpenAPIToolGenerator(document, options);
|
|
2215
2922
|
} catch (error) {
|
|
2923
|
+
if (error instanceof OverlayError) {
|
|
2924
|
+
throw error;
|
|
2925
|
+
}
|
|
2216
2926
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2217
2927
|
throw new LoadError(`Failed to load OpenAPI spec from file: ${errorMessage}`, {
|
|
2218
2928
|
filePath,
|
|
@@ -2228,6 +2938,9 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
|
2228
2938
|
const document = yaml.parse(yamlString);
|
|
2229
2939
|
return new _OpenAPIToolGenerator(document, options);
|
|
2230
2940
|
} catch (error) {
|
|
2941
|
+
if (error instanceof OverlayError) {
|
|
2942
|
+
throw error;
|
|
2943
|
+
}
|
|
2231
2944
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2232
2945
|
throw new ParseError(`Failed to parse YAML: ${errorMessage}`, {
|
|
2233
2946
|
originalError: error
|
|
@@ -2254,6 +2967,16 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
|
2254
2967
|
const validator = new Validator();
|
|
2255
2968
|
return validator.validate(this.document);
|
|
2256
2969
|
}
|
|
2970
|
+
/**
|
|
2971
|
+
* Lint the loaded document for agent-readiness (missing operationIds,
|
|
2972
|
+
* vague descriptions, unpaginated lists, oversized schemas, ...). Runs
|
|
2973
|
+
* after overlays and dereferencing so findings reflect what tools would
|
|
2974
|
+
* actually be generated from.
|
|
2975
|
+
*/
|
|
2976
|
+
async lint() {
|
|
2977
|
+
await this.initialize(false);
|
|
2978
|
+
return lintDocument(this.getDocument());
|
|
2979
|
+
}
|
|
2257
2980
|
// NOTE: internal/private-address blocking + IPv4-mapped-IPv6 decoding now live
|
|
2258
2981
|
// in `ssrf.ts` (`isBlockedHostname` / `isBlockedAddress` / `decodeIpv4MappedIpv6`),
|
|
2259
2982
|
// shared by the spec-URL fetch (`fromURL`) and the `$ref` resolver below, and
|
|
@@ -2391,7 +3114,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
|
2391
3114
|
/**
|
|
2392
3115
|
* Initialize the generator (dereference if needed, then validate)
|
|
2393
3116
|
*/
|
|
2394
|
-
async initialize() {
|
|
3117
|
+
async initialize(runValidation = this.options.validate) {
|
|
2395
3118
|
if (this.options.dereference && !this.dereferencedDocument) {
|
|
2396
3119
|
const cloned = JSON.parse(JSON.stringify(this.document));
|
|
2397
3120
|
if (!_OpenAPIToolGenerator.hasExternalRefs(cloned)) {
|
|
@@ -2409,7 +3132,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
|
2409
3132
|
}
|
|
2410
3133
|
}
|
|
2411
3134
|
}
|
|
2412
|
-
if (
|
|
3135
|
+
if (runValidation) {
|
|
2413
3136
|
const validator = new Validator();
|
|
2414
3137
|
const documentToValidate = this.dereferencedDocument ?? this.document;
|
|
2415
3138
|
const result = await validator.validate(documentToValidate);
|
|
@@ -2507,7 +3230,7 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
|
2507
3230
|
const outputSchema = responseBuilder.build(operation.responses);
|
|
2508
3231
|
const overrides = extractExtensionOverrides(operation);
|
|
2509
3232
|
const name = this.generateToolName(pathStr, method, overrides.name ?? operation.operationId, options);
|
|
2510
|
-
const description = overrides.description ?? (operation
|
|
3233
|
+
const description = overrides.description ?? composeDescription(operation, method, pathStr, options.descriptionStrategy ?? "summaryOnly");
|
|
2511
3234
|
const title = overrides.title ?? operation.summary;
|
|
2512
3235
|
const inferred = options.inferAnnotations !== false ? inferAnnotationsFromMethod(method.toLowerCase()) : void 0;
|
|
2513
3236
|
const annotations = inferred || overrides.annotations ? { ...inferred, ...overrides.annotations } : void 0;
|
|
@@ -2524,16 +3247,57 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
|
2524
3247
|
if (resolvedOutputSchema) {
|
|
2525
3248
|
resolvedOutputSchema = SchemaBuilder.truncateDepth(resolvedOutputSchema, maxSchemaDepth);
|
|
2526
3249
|
}
|
|
3250
|
+
const applyTrim = (schema, isInputRoot) => {
|
|
3251
|
+
let trimmed = schema;
|
|
3252
|
+
if (options.stripExamples) trimmed = SchemaBuilder.stripExamples(trimmed);
|
|
3253
|
+
if (options.maxDescriptionLength !== void 0) {
|
|
3254
|
+
trimmed = SchemaBuilder.capDescriptions(trimmed, options.maxDescriptionLength);
|
|
3255
|
+
}
|
|
3256
|
+
if (options.maxProperties !== void 0) {
|
|
3257
|
+
if (isInputRoot) {
|
|
3258
|
+
const properties = trimmed.properties;
|
|
3259
|
+
if (properties && typeof properties === "object") {
|
|
3260
|
+
const limited = {};
|
|
3261
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
3262
|
+
limited[key] = SchemaBuilder.limitProperties(value, options.maxProperties);
|
|
3263
|
+
}
|
|
3264
|
+
trimmed = { ...trimmed, properties: limited };
|
|
3265
|
+
}
|
|
3266
|
+
} else {
|
|
3267
|
+
trimmed = SchemaBuilder.limitProperties(trimmed, options.maxProperties);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
return trimmed;
|
|
3271
|
+
};
|
|
3272
|
+
if (options.stripExamples || options.maxProperties !== void 0 || options.maxDescriptionLength !== void 0) {
|
|
3273
|
+
resolvedInputSchema = applyTrim(resolvedInputSchema, true);
|
|
3274
|
+
if (resolvedOutputSchema) {
|
|
3275
|
+
resolvedOutputSchema = applyTrim(resolvedOutputSchema, false);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
2527
3278
|
if (options.target) {
|
|
2528
3279
|
resolvedInputSchema = applyClientTarget(resolvedInputSchema, options.target);
|
|
2529
3280
|
if (resolvedOutputSchema) {
|
|
2530
3281
|
resolvedOutputSchema = applyClientTarget(resolvedOutputSchema, options.target);
|
|
2531
3282
|
}
|
|
2532
3283
|
}
|
|
3284
|
+
const responseHints = detectResponseHints(resolvedOutputSchema, mapper);
|
|
3285
|
+
if (responseHints) {
|
|
3286
|
+
metadata.responseHints = responseHints;
|
|
3287
|
+
}
|
|
3288
|
+
let finalDescription = description;
|
|
3289
|
+
if (options.appendResponseSummary && resolvedOutputSchema) {
|
|
3290
|
+
const summary = summarizeOutputSchema(resolvedOutputSchema);
|
|
3291
|
+
if (summary) {
|
|
3292
|
+
finalDescription = `${finalDescription}
|
|
3293
|
+
|
|
3294
|
+
Returns: ${summary}`;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
2533
3297
|
return {
|
|
2534
3298
|
name,
|
|
2535
3299
|
...title !== void 0 && { title },
|
|
2536
|
-
description,
|
|
3300
|
+
description: finalDescription,
|
|
2537
3301
|
...annotations && { annotations },
|
|
2538
3302
|
inputSchema: resolvedInputSchema,
|
|
2539
3303
|
outputSchema: resolvedOutputSchema,
|
|
@@ -2609,7 +3373,9 @@ var OpenAPIToolGenerator = class _OpenAPIToolGenerator {
|
|
|
2609
3373
|
} else if (operationId) {
|
|
2610
3374
|
rawName = operationId;
|
|
2611
3375
|
} else {
|
|
2612
|
-
const sanitized =
|
|
3376
|
+
const sanitized = trimUnderscores(
|
|
3377
|
+
path.replace(/\{([^{}]+)\}/g, "By_$1").replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_")
|
|
3378
|
+
);
|
|
2613
3379
|
rawName = `${method}_${sanitized}`;
|
|
2614
3380
|
}
|
|
2615
3381
|
return normalizeToolName(
|
|
@@ -2824,7 +3590,7 @@ var SecurityResolver = class {
|
|
|
2824
3590
|
resolveDigestAuth(context) {
|
|
2825
3591
|
const digest = context.digest;
|
|
2826
3592
|
if (!digest) return void 0;
|
|
2827
|
-
const quoted = (v) => String(v).replace(/[\r\n]/g, "").replace(/"/g, '\\"');
|
|
3593
|
+
const quoted = (v) => String(v).replace(/[\r\n]/g, "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2828
3594
|
const token = (v) => String(v).replace(/[\r\n",]/g, "");
|
|
2829
3595
|
const parts = [
|
|
2830
3596
|
`username="${quoted(digest.username)}"`,
|
|
@@ -3285,6 +4051,44 @@ function toSdkTool(tool, wrapper) {
|
|
|
3285
4051
|
}
|
|
3286
4052
|
];
|
|
3287
4053
|
}
|
|
4054
|
+
|
|
4055
|
+
// src/token-report.ts
|
|
4056
|
+
function estimateToolTokens(tool) {
|
|
4057
|
+
const advertised = {
|
|
4058
|
+
name: tool.name,
|
|
4059
|
+
...tool.title !== void 0 && { title: tool.title },
|
|
4060
|
+
description: tool.description,
|
|
4061
|
+
...tool.annotations !== void 0 && { annotations: tool.annotations },
|
|
4062
|
+
inputSchema: tool.inputSchema,
|
|
4063
|
+
...tool.outputSchema !== void 0 && { outputSchema: tool.outputSchema }
|
|
4064
|
+
};
|
|
4065
|
+
return Math.ceil(JSON.stringify(advertised).length / 4);
|
|
4066
|
+
}
|
|
4067
|
+
function analyzeToolSet(tools, options = {}) {
|
|
4068
|
+
const tokenBudget = options.tokenBudget ?? 1e4;
|
|
4069
|
+
const maxRecommendedTools = options.maxRecommendedTools ?? 40;
|
|
4070
|
+
const perToolWarning = options.perToolWarning ?? 2e3;
|
|
4071
|
+
const perTool = tools.map((tool) => ({ name: tool.name, tokens: estimateToolTokens(tool) })).sort((a, b) => b.tokens - a.tokens || (a.name < b.name ? -1 : 1));
|
|
4072
|
+
const estimatedTokens = perTool.reduce((sum, entry) => sum + entry.tokens, 0);
|
|
4073
|
+
const warnings = [];
|
|
4074
|
+
if (tools.length > maxRecommendedTools) {
|
|
4075
|
+
warnings.push(
|
|
4076
|
+
`${tools.length} tools exceeds the ~${maxRecommendedTools}-tool range where model selection accuracy degrades \u2014 curate with filters (tags, paths, readOnlyOnly) or split into focused servers.`
|
|
4077
|
+
);
|
|
4078
|
+
}
|
|
4079
|
+
if (estimatedTokens > tokenBudget) {
|
|
4080
|
+
warnings.push(
|
|
4081
|
+
`Estimated ${estimatedTokens} tokens of tool definitions exceeds the ${tokenBudget}-token budget \u2014 trim schemas (maxSchemaDepth, maxProperties) or reduce the tool count.`
|
|
4082
|
+
);
|
|
4083
|
+
}
|
|
4084
|
+
const heavy = perTool.filter((entry) => entry.tokens > perToolWarning);
|
|
4085
|
+
if (heavy.length > 0) {
|
|
4086
|
+
warnings.push(
|
|
4087
|
+
`${heavy.length} tool(s) exceed ${perToolWarning} tokens each (${heavy.slice(0, 3).map((entry) => `${entry.name}: ~${entry.tokens}`).join(", ")}${heavy.length > 3 ? ", \u2026" : ""}) \u2014 consider schema trimming for these.`
|
|
4088
|
+
);
|
|
4089
|
+
}
|
|
4090
|
+
return { toolCount: tools.length, estimatedTokens, perTool, warnings };
|
|
4091
|
+
}
|
|
3288
4092
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3289
4093
|
0 && (module.exports = {
|
|
3290
4094
|
BLOCKED_HOSTNAMES,
|
|
@@ -3293,6 +4097,7 @@ function toSdkTool(tool, wrapper) {
|
|
|
3293
4097
|
LoadError,
|
|
3294
4098
|
OpenAPIToolError,
|
|
3295
4099
|
OpenAPIToolGenerator,
|
|
4100
|
+
OverlayError,
|
|
3296
4101
|
ParameterResolver,
|
|
3297
4102
|
ParseError,
|
|
3298
4103
|
RequestBuildError,
|
|
@@ -3303,7 +4108,9 @@ function toSdkTool(tool, wrapper) {
|
|
|
3303
4108
|
SsrfError,
|
|
3304
4109
|
ValidationError,
|
|
3305
4110
|
Validator,
|
|
4111
|
+
analyzeToolSet,
|
|
3306
4112
|
applyClientTarget,
|
|
4113
|
+
applyOverlay,
|
|
3307
4114
|
assertUrlSafe,
|
|
3308
4115
|
buildHttpRequest,
|
|
3309
4116
|
collapseNestedUnions,
|
|
@@ -3314,12 +4121,14 @@ function toSdkTool(tool, wrapper) {
|
|
|
3314
4121
|
demoteFormats,
|
|
3315
4122
|
enforceClosedObjects,
|
|
3316
4123
|
ensureArrayItems,
|
|
4124
|
+
estimateToolTokens,
|
|
3317
4125
|
extractExtensionOverrides,
|
|
3318
4126
|
inferAnnotationsFromMethod,
|
|
3319
4127
|
inlineLocalRefs,
|
|
3320
4128
|
isBlockedAddress,
|
|
3321
4129
|
isBlockedHostname,
|
|
3322
4130
|
isReferenceObject,
|
|
4131
|
+
lintDocument,
|
|
3323
4132
|
normalizeSsrfOptions,
|
|
3324
4133
|
requireAllProperties,
|
|
3325
4134
|
resolveExtensionEnabled,
|