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/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, "_")
|
|
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 (
|
|
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
|
|
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 =
|
|
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,
|