@agentcash/discovery 0.1.0 → 0.1.2
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/AGENTS.md +0 -1
- package/README.md +139 -57
- package/dist/cli.cjs +607 -72
- package/dist/cli.js +607 -72
- package/dist/index.cjs +651 -4
- package/dist/index.d.cts +187 -1
- package/dist/index.d.ts +187 -1
- package/dist/index.js +643 -3
- package/package.json +2 -3
- package/.claude/CLAUDE.md +0 -24
package/dist/index.js
CHANGED
|
@@ -11,6 +11,9 @@ var STRICT_ESCALATION_CODES = [
|
|
|
11
11
|
"INTEROP_MPP_USED"
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
+
// src/mmm-enabled.ts
|
|
15
|
+
var isMmmEnabled = () => "0.1.2".includes("-mmm");
|
|
16
|
+
|
|
14
17
|
// src/core/constants.ts
|
|
15
18
|
var OPENAPI_PATH_CANDIDATES = ["/openapi.json", "/.well-known/openapi.json"];
|
|
16
19
|
var WELL_KNOWN_MPP_PATH = "/.well-known/mpp";
|
|
@@ -963,7 +966,7 @@ function detectProtocols(response) {
|
|
|
963
966
|
}
|
|
964
967
|
const authHeader = response.headers.get("www-authenticate")?.toLowerCase() ?? "";
|
|
965
968
|
if (authHeader.includes("x402")) protocols.add("x402");
|
|
966
|
-
if (authHeader.includes("mpp")) protocols.add("mpp");
|
|
969
|
+
if (isMmmEnabled() && authHeader.includes("mpp")) protocols.add("mpp");
|
|
967
970
|
return [...protocols];
|
|
968
971
|
}
|
|
969
972
|
async function runProbeStage(options) {
|
|
@@ -1295,7 +1298,7 @@ async function runDiscovery({
|
|
|
1295
1298
|
includeRaw
|
|
1296
1299
|
})
|
|
1297
1300
|
);
|
|
1298
|
-
if (includeInteropMpp) {
|
|
1301
|
+
if (includeInteropMpp && isMmmEnabled()) {
|
|
1299
1302
|
stages.push(
|
|
1300
1303
|
() => runInteropMppStage({
|
|
1301
1304
|
origin,
|
|
@@ -1397,6 +1400,636 @@ async function runDiscovery({
|
|
|
1397
1400
|
};
|
|
1398
1401
|
}
|
|
1399
1402
|
|
|
1403
|
+
// src/adapters/mcp.ts
|
|
1404
|
+
var DEFAULT_GUIDANCE_AUTO_INCLUDE_MAX_TOKENS = 1e3;
|
|
1405
|
+
var OPENAPI_METHODS = [
|
|
1406
|
+
"GET",
|
|
1407
|
+
"POST",
|
|
1408
|
+
"PUT",
|
|
1409
|
+
"DELETE",
|
|
1410
|
+
"PATCH",
|
|
1411
|
+
"HEAD",
|
|
1412
|
+
"OPTIONS",
|
|
1413
|
+
"TRACE"
|
|
1414
|
+
];
|
|
1415
|
+
function isRecord4(value) {
|
|
1416
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
1417
|
+
}
|
|
1418
|
+
function toFiniteNumber(value) {
|
|
1419
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1420
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1421
|
+
const parsed = Number(value);
|
|
1422
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
1423
|
+
}
|
|
1424
|
+
return void 0;
|
|
1425
|
+
}
|
|
1426
|
+
function formatPriceHintFromResource(resource) {
|
|
1427
|
+
const pricing = resource.pricing;
|
|
1428
|
+
if (pricing) {
|
|
1429
|
+
if (pricing.pricingMode === "fixed" && pricing.price) {
|
|
1430
|
+
return pricing.price.startsWith("$") ? pricing.price : `$${pricing.price}`;
|
|
1431
|
+
}
|
|
1432
|
+
if (pricing.pricingMode === "range" && pricing.minPrice && pricing.maxPrice) {
|
|
1433
|
+
const min = pricing.minPrice.startsWith("$") ? pricing.minPrice : `$${pricing.minPrice}`;
|
|
1434
|
+
const max = pricing.maxPrice.startsWith("$") ? pricing.maxPrice : `$${pricing.maxPrice}`;
|
|
1435
|
+
return `${min}-${max}`;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (!resource.priceHint) return void 0;
|
|
1439
|
+
const hint = resource.priceHint.trim();
|
|
1440
|
+
if (hint.length === 0) return void 0;
|
|
1441
|
+
if (hint.startsWith("$")) return hint;
|
|
1442
|
+
if (hint.includes("-")) {
|
|
1443
|
+
const [min, max] = hint.split("-", 2).map((part) => part.trim());
|
|
1444
|
+
if (min && max) return `$${min}-$${max}`;
|
|
1445
|
+
}
|
|
1446
|
+
return `$${hint}`;
|
|
1447
|
+
}
|
|
1448
|
+
function deriveMcpAuthMode(resource) {
|
|
1449
|
+
if (!resource) return void 0;
|
|
1450
|
+
const hint = resource.authHint;
|
|
1451
|
+
if (hint === "paid" || hint === "siwx" || hint === "apiKey" || hint === "unprotected") {
|
|
1452
|
+
return hint;
|
|
1453
|
+
}
|
|
1454
|
+
return void 0;
|
|
1455
|
+
}
|
|
1456
|
+
function inferFailureCause(warnings) {
|
|
1457
|
+
const openapiWarnings = warnings.filter((w) => w.stage === "openapi");
|
|
1458
|
+
const parseWarning = openapiWarnings.find(
|
|
1459
|
+
(w) => w.code === "PARSE_FAILED" || w.code === "OPENAPI_TOP_LEVEL_INVALID" || w.code === "OPENAPI_OPERATION_INVALID" || w.code === "OPENAPI_PRICING_INVALID"
|
|
1460
|
+
);
|
|
1461
|
+
if (parseWarning) {
|
|
1462
|
+
return { cause: "parse", message: parseWarning.message };
|
|
1463
|
+
}
|
|
1464
|
+
const fetchWarning = openapiWarnings.find((w) => w.code === "FETCH_FAILED");
|
|
1465
|
+
if (fetchWarning) {
|
|
1466
|
+
const msg = fetchWarning.message.toLowerCase();
|
|
1467
|
+
if (msg.includes("timeout") || msg.includes("timed out") || msg.includes("abort")) {
|
|
1468
|
+
return { cause: "timeout", message: fetchWarning.message };
|
|
1469
|
+
}
|
|
1470
|
+
return { cause: "network", message: fetchWarning.message };
|
|
1471
|
+
}
|
|
1472
|
+
return { cause: "not_found" };
|
|
1473
|
+
}
|
|
1474
|
+
function getOpenApiInfo(document) {
|
|
1475
|
+
if (!isRecord4(document) || !isRecord4(document.info)) return void 0;
|
|
1476
|
+
if (typeof document.info.title !== "string") return void 0;
|
|
1477
|
+
return {
|
|
1478
|
+
title: document.info.title,
|
|
1479
|
+
...typeof document.info.version === "string" ? { version: document.info.version } : {},
|
|
1480
|
+
...typeof document.info.description === "string" ? { description: document.info.description } : {}
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function getGuidanceUrl(result) {
|
|
1484
|
+
const fromTrace = result.trace.find((entry) => entry.links?.llmsTxtUrl)?.links?.llmsTxtUrl;
|
|
1485
|
+
if (fromTrace) return fromTrace;
|
|
1486
|
+
const fromResource = result.resources.find((resource) => resource.links?.llmsTxtUrl)?.links?.llmsTxtUrl;
|
|
1487
|
+
if (fromResource) return fromResource;
|
|
1488
|
+
return void 0;
|
|
1489
|
+
}
|
|
1490
|
+
async function resolveGuidance(options) {
|
|
1491
|
+
let guidanceText = typeof options.result.rawSources?.llmsTxt === "string" ? options.result.rawSources.llmsTxt : void 0;
|
|
1492
|
+
if (!guidanceText) {
|
|
1493
|
+
const guidanceUrl = getGuidanceUrl(options.result) ?? `${options.result.origin}/llms.txt`;
|
|
1494
|
+
const fetcher = options.fetcher ?? fetch;
|
|
1495
|
+
try {
|
|
1496
|
+
const response = await fetcher(guidanceUrl, {
|
|
1497
|
+
method: "GET",
|
|
1498
|
+
headers: { Accept: "text/plain", ...options.headers },
|
|
1499
|
+
signal: options.signal
|
|
1500
|
+
});
|
|
1501
|
+
if (response.ok) guidanceText = await response.text();
|
|
1502
|
+
} catch {
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (!guidanceText) {
|
|
1506
|
+
return { guidanceAvailable: false };
|
|
1507
|
+
}
|
|
1508
|
+
const guidanceTokens = estimateTokenCount(guidanceText);
|
|
1509
|
+
const threshold = options.guidanceAutoIncludeMaxTokens ?? DEFAULT_GUIDANCE_AUTO_INCLUDE_MAX_TOKENS;
|
|
1510
|
+
const shouldInclude = options.includeGuidance === true || options.includeGuidance == null && guidanceTokens <= threshold;
|
|
1511
|
+
return {
|
|
1512
|
+
guidanceAvailable: true,
|
|
1513
|
+
guidanceTokens,
|
|
1514
|
+
...shouldInclude ? { guidance: guidanceText } : {}
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
function extractPathsDocument(document) {
|
|
1518
|
+
if (!isRecord4(document)) return void 0;
|
|
1519
|
+
if (!isRecord4(document.paths)) return void 0;
|
|
1520
|
+
return document.paths;
|
|
1521
|
+
}
|
|
1522
|
+
function findMatchingOpenApiPath(paths, targetPath) {
|
|
1523
|
+
const exact = paths[targetPath];
|
|
1524
|
+
if (isRecord4(exact)) return { matchedPath: targetPath, pathItem: exact };
|
|
1525
|
+
for (const [specPath, entry] of Object.entries(paths)) {
|
|
1526
|
+
if (!isRecord4(entry)) continue;
|
|
1527
|
+
const pattern = specPath.replace(/\{[^}]+\}/g, "[^/]+");
|
|
1528
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
1529
|
+
if (regex.test(targetPath)) {
|
|
1530
|
+
return { matchedPath: specPath, pathItem: entry };
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
function resolveRef(document, ref, seen) {
|
|
1536
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
1537
|
+
if (seen.has(ref)) return { $circular: ref };
|
|
1538
|
+
seen.add(ref);
|
|
1539
|
+
const parts = ref.slice(2).split("/");
|
|
1540
|
+
let current = document;
|
|
1541
|
+
for (const part of parts) {
|
|
1542
|
+
if (!isRecord4(current)) return void 0;
|
|
1543
|
+
current = current[part];
|
|
1544
|
+
if (current === void 0) return void 0;
|
|
1545
|
+
}
|
|
1546
|
+
if (isRecord4(current)) return resolveRefs(document, current, seen);
|
|
1547
|
+
return current;
|
|
1548
|
+
}
|
|
1549
|
+
function resolveRefs(document, obj, seen, depth = 0) {
|
|
1550
|
+
if (depth > 4) return obj;
|
|
1551
|
+
const resolved = {};
|
|
1552
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1553
|
+
if (key === "$ref" && typeof value === "string") {
|
|
1554
|
+
const deref = resolveRef(document, value, seen);
|
|
1555
|
+
if (isRecord4(deref)) {
|
|
1556
|
+
Object.assign(resolved, deref);
|
|
1557
|
+
} else {
|
|
1558
|
+
resolved[key] = value;
|
|
1559
|
+
}
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
if (isRecord4(value)) {
|
|
1563
|
+
resolved[key] = resolveRefs(document, value, seen, depth + 1);
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
if (Array.isArray(value)) {
|
|
1567
|
+
resolved[key] = value.map(
|
|
1568
|
+
(item) => isRecord4(item) ? resolveRefs(document, item, seen, depth + 1) : item
|
|
1569
|
+
);
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
resolved[key] = value;
|
|
1573
|
+
}
|
|
1574
|
+
return resolved;
|
|
1575
|
+
}
|
|
1576
|
+
function extractRequestBodySchema(operationSchema) {
|
|
1577
|
+
const requestBody = operationSchema.requestBody;
|
|
1578
|
+
if (!isRecord4(requestBody)) return void 0;
|
|
1579
|
+
const content = requestBody.content;
|
|
1580
|
+
if (!isRecord4(content)) return void 0;
|
|
1581
|
+
const jsonMediaType = content["application/json"];
|
|
1582
|
+
if (isRecord4(jsonMediaType) && isRecord4(jsonMediaType.schema)) {
|
|
1583
|
+
return jsonMediaType.schema;
|
|
1584
|
+
}
|
|
1585
|
+
for (const mediaType of Object.values(content)) {
|
|
1586
|
+
if (isRecord4(mediaType) && isRecord4(mediaType.schema)) {
|
|
1587
|
+
return mediaType.schema;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return void 0;
|
|
1591
|
+
}
|
|
1592
|
+
function extractParameters(operationSchema) {
|
|
1593
|
+
const params = operationSchema.parameters;
|
|
1594
|
+
if (!Array.isArray(params)) return [];
|
|
1595
|
+
return params.filter((value) => isRecord4(value));
|
|
1596
|
+
}
|
|
1597
|
+
function extractInputSchema(operationSchema) {
|
|
1598
|
+
const requestBody = extractRequestBodySchema(operationSchema);
|
|
1599
|
+
const parameters = extractParameters(operationSchema);
|
|
1600
|
+
if (!requestBody && parameters.length === 0) return void 0;
|
|
1601
|
+
if (requestBody && parameters.length === 0) return requestBody;
|
|
1602
|
+
return {
|
|
1603
|
+
...requestBody ? { requestBody } : {},
|
|
1604
|
+
...parameters.length > 0 ? { parameters } : {}
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
function parseOperationPrice(operation) {
|
|
1608
|
+
const paymentInfo = operation["x-payment-info"];
|
|
1609
|
+
if (!isRecord4(paymentInfo)) return void 0;
|
|
1610
|
+
const fixed = toFiniteNumber(paymentInfo.price);
|
|
1611
|
+
if (fixed != null) return `$${String(fixed)}`;
|
|
1612
|
+
const min = toFiniteNumber(paymentInfo.minPrice);
|
|
1613
|
+
const max = toFiniteNumber(paymentInfo.maxPrice);
|
|
1614
|
+
if (min != null && max != null) return `$${String(min)}-$${String(max)}`;
|
|
1615
|
+
return void 0;
|
|
1616
|
+
}
|
|
1617
|
+
function parseOperationProtocols(operation) {
|
|
1618
|
+
const paymentInfo = operation["x-payment-info"];
|
|
1619
|
+
if (!isRecord4(paymentInfo) || !Array.isArray(paymentInfo.protocols)) return void 0;
|
|
1620
|
+
const protocols = paymentInfo.protocols.filter(
|
|
1621
|
+
(protocol) => typeof protocol === "string" && protocol.length > 0
|
|
1622
|
+
);
|
|
1623
|
+
return protocols.length > 0 ? protocols : void 0;
|
|
1624
|
+
}
|
|
1625
|
+
function parseOperationAuthMode(operation) {
|
|
1626
|
+
const authExtension = operation["x-agentcash-auth"];
|
|
1627
|
+
if (isRecord4(authExtension)) {
|
|
1628
|
+
const mode = authExtension.mode;
|
|
1629
|
+
if (mode === "paid" || mode === "siwx" || mode === "apiKey" || mode === "unprotected") {
|
|
1630
|
+
return mode;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
if (isRecord4(operation["x-payment-info"])) return "paid";
|
|
1634
|
+
return void 0;
|
|
1635
|
+
}
|
|
1636
|
+
function createResourceMap(result) {
|
|
1637
|
+
const map = /* @__PURE__ */ new Map();
|
|
1638
|
+
for (const resource of result.resources) {
|
|
1639
|
+
map.set(resource.resourceKey, resource);
|
|
1640
|
+
}
|
|
1641
|
+
return map;
|
|
1642
|
+
}
|
|
1643
|
+
function toMcpEndpointSummary(resource) {
|
|
1644
|
+
const method = parseMethod(resource.method);
|
|
1645
|
+
if (!method) return void 0;
|
|
1646
|
+
const price = formatPriceHintFromResource(resource);
|
|
1647
|
+
const authMode = deriveMcpAuthMode(resource);
|
|
1648
|
+
return {
|
|
1649
|
+
path: resource.path,
|
|
1650
|
+
method,
|
|
1651
|
+
summary: resource.summary ?? `${method} ${resource.path}`,
|
|
1652
|
+
...price ? { price } : {},
|
|
1653
|
+
...resource.protocolHints?.length ? { protocols: [...resource.protocolHints] } : {},
|
|
1654
|
+
...authMode ? { authMode } : {}
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
async function discoverForMcp(options) {
|
|
1658
|
+
const result = await runDiscovery({
|
|
1659
|
+
detailed: true,
|
|
1660
|
+
options: {
|
|
1661
|
+
target: options.target,
|
|
1662
|
+
compatMode: options.compatMode ?? "off",
|
|
1663
|
+
rawView: "full",
|
|
1664
|
+
fetcher: options.fetcher,
|
|
1665
|
+
headers: options.headers,
|
|
1666
|
+
signal: options.signal
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
if (result.resources.length === 0) {
|
|
1670
|
+
const failure = inferFailureCause(result.warnings);
|
|
1671
|
+
return {
|
|
1672
|
+
found: false,
|
|
1673
|
+
origin: result.origin,
|
|
1674
|
+
cause: failure.cause,
|
|
1675
|
+
...failure.message ? { message: failure.message } : {},
|
|
1676
|
+
warnings: result.warnings
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
const source = result.selectedStage ?? "openapi";
|
|
1680
|
+
const endpoints = result.resources.map(toMcpEndpointSummary).filter((endpoint) => endpoint != null).sort((a, b) => {
|
|
1681
|
+
if (a.path !== b.path) return a.path.localeCompare(b.path);
|
|
1682
|
+
return a.method.localeCompare(b.method);
|
|
1683
|
+
});
|
|
1684
|
+
const guidance = await resolveGuidance({
|
|
1685
|
+
result,
|
|
1686
|
+
includeGuidance: options.includeGuidance,
|
|
1687
|
+
guidanceAutoIncludeMaxTokens: options.guidanceAutoIncludeMaxTokens,
|
|
1688
|
+
fetcher: options.fetcher,
|
|
1689
|
+
headers: options.headers,
|
|
1690
|
+
signal: options.signal
|
|
1691
|
+
});
|
|
1692
|
+
return {
|
|
1693
|
+
found: true,
|
|
1694
|
+
origin: result.origin,
|
|
1695
|
+
source,
|
|
1696
|
+
...getOpenApiInfo(result.rawSources?.openapi) ? { info: getOpenApiInfo(result.rawSources?.openapi) } : {},
|
|
1697
|
+
...guidance,
|
|
1698
|
+
endpoints,
|
|
1699
|
+
warnings: result.warnings
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
async function inspectEndpointForMcp(options) {
|
|
1703
|
+
const endpoint = new URL(options.endpointUrl);
|
|
1704
|
+
const origin = normalizeOrigin(endpoint.origin);
|
|
1705
|
+
const path = normalizePath(endpoint.pathname || "/");
|
|
1706
|
+
const result = await runDiscovery({
|
|
1707
|
+
detailed: true,
|
|
1708
|
+
options: {
|
|
1709
|
+
target: options.target,
|
|
1710
|
+
compatMode: options.compatMode ?? "off",
|
|
1711
|
+
rawView: "full",
|
|
1712
|
+
fetcher: options.fetcher,
|
|
1713
|
+
headers: options.headers,
|
|
1714
|
+
signal: options.signal
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
const document = result.rawSources?.openapi;
|
|
1718
|
+
const paths = extractPathsDocument(document);
|
|
1719
|
+
const resourceMap = createResourceMap(result);
|
|
1720
|
+
const advisories = {};
|
|
1721
|
+
const specMethods = [];
|
|
1722
|
+
if (paths) {
|
|
1723
|
+
const matched = findMatchingOpenApiPath(paths, path);
|
|
1724
|
+
if (matched) {
|
|
1725
|
+
for (const candidate of OPENAPI_METHODS) {
|
|
1726
|
+
const operation = matched.pathItem[candidate.toLowerCase()];
|
|
1727
|
+
if (!isRecord4(operation) || !isRecord4(document)) continue;
|
|
1728
|
+
specMethods.push(candidate);
|
|
1729
|
+
const resolvedOperation = resolveRefs(document, operation, /* @__PURE__ */ new Set());
|
|
1730
|
+
const resource = resourceMap.get(toResourceKey(origin, candidate, matched.matchedPath));
|
|
1731
|
+
const formattedPrice = resource ? formatPriceHintFromResource(resource) : void 0;
|
|
1732
|
+
const operationSummary = typeof resolvedOperation.summary === "string" ? resolvedOperation.summary : typeof resolvedOperation.description === "string" ? resolvedOperation.description : void 0;
|
|
1733
|
+
const operationPrice = parseOperationPrice(resolvedOperation);
|
|
1734
|
+
const operationProtocols = parseOperationProtocols(resolvedOperation);
|
|
1735
|
+
const operationAuthMode = parseOperationAuthMode(resolvedOperation);
|
|
1736
|
+
const inputSchema = extractInputSchema(resolvedOperation);
|
|
1737
|
+
advisories[candidate] = {
|
|
1738
|
+
method: candidate,
|
|
1739
|
+
...resource?.summary || operationSummary ? {
|
|
1740
|
+
summary: resource?.summary ?? operationSummary
|
|
1741
|
+
} : {},
|
|
1742
|
+
...formattedPrice || operationPrice ? {
|
|
1743
|
+
estimatedPrice: formattedPrice ?? operationPrice
|
|
1744
|
+
} : {},
|
|
1745
|
+
...resource?.protocolHints?.length || operationProtocols ? {
|
|
1746
|
+
protocols: resource?.protocolHints ?? operationProtocols
|
|
1747
|
+
} : {},
|
|
1748
|
+
...deriveMcpAuthMode(resource) || operationAuthMode ? {
|
|
1749
|
+
authMode: deriveMcpAuthMode(resource) ?? operationAuthMode
|
|
1750
|
+
} : {},
|
|
1751
|
+
...inputSchema ? { inputSchema } : {},
|
|
1752
|
+
operationSchema: resolvedOperation
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (specMethods.length === 0) {
|
|
1758
|
+
for (const method of OPENAPI_METHODS) {
|
|
1759
|
+
const resource = resourceMap.get(toResourceKey(origin, method, path));
|
|
1760
|
+
if (!resource) continue;
|
|
1761
|
+
specMethods.push(method);
|
|
1762
|
+
const formattedPrice = formatPriceHintFromResource(resource);
|
|
1763
|
+
advisories[method] = {
|
|
1764
|
+
method,
|
|
1765
|
+
...resource.summary ? { summary: resource.summary } : {},
|
|
1766
|
+
...formattedPrice ? { estimatedPrice: formattedPrice } : {},
|
|
1767
|
+
...resource.protocolHints?.length ? { protocols: [...resource.protocolHints] } : {},
|
|
1768
|
+
...deriveMcpAuthMode(resource) ? { authMode: deriveMcpAuthMode(resource) } : {}
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
if (specMethods.length === 0) {
|
|
1773
|
+
const failure = inferFailureCause(result.warnings);
|
|
1774
|
+
return {
|
|
1775
|
+
found: false,
|
|
1776
|
+
origin,
|
|
1777
|
+
path,
|
|
1778
|
+
cause: failure.cause,
|
|
1779
|
+
...failure.message ? { message: failure.message } : {},
|
|
1780
|
+
warnings: result.warnings
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
return {
|
|
1784
|
+
found: true,
|
|
1785
|
+
origin,
|
|
1786
|
+
path,
|
|
1787
|
+
specMethods,
|
|
1788
|
+
advisories,
|
|
1789
|
+
warnings: result.warnings
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/harness.ts
|
|
1794
|
+
var INTENT_TRIGGERS = ["x402", "mpp", "pay for", "micropayment", "agentic commerce"];
|
|
1795
|
+
var CLIENT_PROFILES = {
|
|
1796
|
+
"claude-code": {
|
|
1797
|
+
id: "claude-code",
|
|
1798
|
+
label: "Claude Code MCP Harness",
|
|
1799
|
+
surface: "mcp",
|
|
1800
|
+
defaultContextWindowTokens: 2e5,
|
|
1801
|
+
zeroHopBudgetPercent: 0.1,
|
|
1802
|
+
notes: "Targets environments where MCP context can use roughly 10% of total context."
|
|
1803
|
+
},
|
|
1804
|
+
"skill-cli": {
|
|
1805
|
+
id: "skill-cli",
|
|
1806
|
+
label: "Skill + CLI Harness",
|
|
1807
|
+
surface: "skill-cli",
|
|
1808
|
+
defaultContextWindowTokens: 2e5,
|
|
1809
|
+
zeroHopBudgetPercent: 0.02,
|
|
1810
|
+
notes: "Targets constrained skill contexts with title/description-heavy routing."
|
|
1811
|
+
},
|
|
1812
|
+
"generic-mcp": {
|
|
1813
|
+
id: "generic-mcp",
|
|
1814
|
+
label: "Generic MCP Harness",
|
|
1815
|
+
surface: "mcp",
|
|
1816
|
+
defaultContextWindowTokens: 128e3,
|
|
1817
|
+
zeroHopBudgetPercent: 0.05,
|
|
1818
|
+
notes: "Conservative MCP profile when specific client budgets are unknown."
|
|
1819
|
+
},
|
|
1820
|
+
generic: {
|
|
1821
|
+
id: "generic",
|
|
1822
|
+
label: "Generic Agent Harness",
|
|
1823
|
+
surface: "generic",
|
|
1824
|
+
defaultContextWindowTokens: 128e3,
|
|
1825
|
+
zeroHopBudgetPercent: 0.03,
|
|
1826
|
+
notes: "Fallback profile for unknown clients."
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
function isFirstPartyDomain(hostname) {
|
|
1830
|
+
const normalized = hostname.toLowerCase();
|
|
1831
|
+
return normalized.endsWith(".dev") && normalized.startsWith("stable");
|
|
1832
|
+
}
|
|
1833
|
+
function safeHostname(origin) {
|
|
1834
|
+
try {
|
|
1835
|
+
return new URL(origin).hostname;
|
|
1836
|
+
} catch {
|
|
1837
|
+
return origin.replace(/^https?:\/\//, "").split("/")[0] ?? origin;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
function previewText(text) {
|
|
1841
|
+
if (!text) return null;
|
|
1842
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
1843
|
+
if (!compact) return null;
|
|
1844
|
+
return compact.length > 220 ? `${compact.slice(0, 217)}...` : compact;
|
|
1845
|
+
}
|
|
1846
|
+
function toAuthModeCount(resources) {
|
|
1847
|
+
const counts = {
|
|
1848
|
+
paid: 0,
|
|
1849
|
+
siwx: 0,
|
|
1850
|
+
apiKey: 0,
|
|
1851
|
+
unprotected: 0,
|
|
1852
|
+
unknown: 0
|
|
1853
|
+
};
|
|
1854
|
+
for (const resource of resources) {
|
|
1855
|
+
const mode = resource.authHint;
|
|
1856
|
+
if (mode === "paid" || mode === "siwx" || mode === "apiKey" || mode === "unprotected") {
|
|
1857
|
+
counts[mode] += 1;
|
|
1858
|
+
} else {
|
|
1859
|
+
counts.unknown += 1;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return counts;
|
|
1863
|
+
}
|
|
1864
|
+
function toSourceCount(resources) {
|
|
1865
|
+
return resources.reduce(
|
|
1866
|
+
(acc, resource) => {
|
|
1867
|
+
acc[resource.source] = (acc[resource.source] ?? 0) + 1;
|
|
1868
|
+
return acc;
|
|
1869
|
+
},
|
|
1870
|
+
{}
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
function toL2Entries(resources) {
|
|
1874
|
+
return [...resources].sort((a, b) => {
|
|
1875
|
+
if (a.path !== b.path) return a.path.localeCompare(b.path);
|
|
1876
|
+
if (a.method !== b.method) return a.method.localeCompare(b.method);
|
|
1877
|
+
return a.resourceKey.localeCompare(b.resourceKey);
|
|
1878
|
+
}).map((resource) => ({
|
|
1879
|
+
resourceKey: resource.resourceKey,
|
|
1880
|
+
method: resource.method,
|
|
1881
|
+
path: resource.path,
|
|
1882
|
+
source: resource.source,
|
|
1883
|
+
authMode: resource.authHint ?? null
|
|
1884
|
+
}));
|
|
1885
|
+
}
|
|
1886
|
+
function getLlmsTxtInfo(result) {
|
|
1887
|
+
const llmsTxtUrl = result.trace.find((entry) => entry.links?.llmsTxtUrl)?.links?.llmsTxtUrl ?? result.resources.find((resource) => resource.links?.llmsTxtUrl)?.links?.llmsTxtUrl ?? null;
|
|
1888
|
+
const llmsTxt = result.rawSources?.llmsTxt;
|
|
1889
|
+
if (llmsTxt) {
|
|
1890
|
+
return {
|
|
1891
|
+
llmsTxtUrl,
|
|
1892
|
+
llmsTxtTokenEstimate: estimateTokenCount(llmsTxt),
|
|
1893
|
+
guidancePreview: previewText(llmsTxt),
|
|
1894
|
+
guidanceStatus: "present"
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
if (llmsTxtUrl) {
|
|
1898
|
+
const failed = result.warnings.some((warning2) => warning2.code === "LLMSTXT_FETCH_FAILED");
|
|
1899
|
+
return {
|
|
1900
|
+
llmsTxtUrl,
|
|
1901
|
+
llmsTxtTokenEstimate: 0,
|
|
1902
|
+
guidancePreview: null,
|
|
1903
|
+
guidanceStatus: failed ? "advertised_but_unfetched" : "missing"
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
return {
|
|
1907
|
+
llmsTxtUrl: null,
|
|
1908
|
+
llmsTxtTokenEstimate: 0,
|
|
1909
|
+
guidancePreview: null,
|
|
1910
|
+
guidanceStatus: "not_advertised"
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
function getHarnessClientProfile(client = "claude-code") {
|
|
1914
|
+
return CLIENT_PROFILES[client] ?? CLIENT_PROFILES.generic;
|
|
1915
|
+
}
|
|
1916
|
+
function listHarnessClientProfiles() {
|
|
1917
|
+
return Object.values(CLIENT_PROFILES);
|
|
1918
|
+
}
|
|
1919
|
+
function buildContextHarnessReport(options) {
|
|
1920
|
+
const client = getHarnessClientProfile(options.client);
|
|
1921
|
+
const contextWindowTokens = options.contextWindowTokens ?? client.defaultContextWindowTokens;
|
|
1922
|
+
const zeroHopBudgetTokens = Math.floor(contextWindowTokens * client.zeroHopBudgetPercent);
|
|
1923
|
+
const hostname = safeHostname(options.result.origin);
|
|
1924
|
+
const domainClass = isFirstPartyDomain(hostname) ? "first-party" : "ugc";
|
|
1925
|
+
const l0Summary = "Route payment-capable intents to agentcash and use install/discover commands for progressive disclosure.";
|
|
1926
|
+
const l1Summary = "Expose installed domain routing hints and trigger fan-out into L2/L3 commands when user intent matches domain capabilities.";
|
|
1927
|
+
const l0EstimatedTokens = estimateTokenCount(
|
|
1928
|
+
`${INTENT_TRIGGERS.join(", ")} npx agentcash install npx agentcash install-ext`
|
|
1929
|
+
);
|
|
1930
|
+
const l1EstimatedTokens = estimateTokenCount(
|
|
1931
|
+
`${hostname} ${l1Summary} npx agentcash discover ${hostname} npx agentcash discover ${hostname} --verbose`
|
|
1932
|
+
);
|
|
1933
|
+
const llmsInfo = getLlmsTxtInfo(options.result);
|
|
1934
|
+
return {
|
|
1935
|
+
target: options.target,
|
|
1936
|
+
origin: options.result.origin,
|
|
1937
|
+
client,
|
|
1938
|
+
budget: {
|
|
1939
|
+
contextWindowTokens,
|
|
1940
|
+
zeroHopBudgetTokens,
|
|
1941
|
+
estimatedZeroHopTokens: l0EstimatedTokens + l1EstimatedTokens,
|
|
1942
|
+
withinBudget: l0EstimatedTokens + l1EstimatedTokens <= zeroHopBudgetTokens
|
|
1943
|
+
},
|
|
1944
|
+
levels: {
|
|
1945
|
+
l0: {
|
|
1946
|
+
layer: "L0",
|
|
1947
|
+
intentTriggers: INTENT_TRIGGERS,
|
|
1948
|
+
installCommand: "npx agentcash install",
|
|
1949
|
+
deliverySurfaces: ["MCP", "Skill+CLI"],
|
|
1950
|
+
summary: l0Summary,
|
|
1951
|
+
estimatedTokens: l0EstimatedTokens
|
|
1952
|
+
},
|
|
1953
|
+
l1: {
|
|
1954
|
+
layer: "L1",
|
|
1955
|
+
domain: hostname,
|
|
1956
|
+
domainClass,
|
|
1957
|
+
selectedDiscoveryStage: options.result.selectedStage ?? null,
|
|
1958
|
+
summary: l1Summary,
|
|
1959
|
+
fanoutCommands: [
|
|
1960
|
+
`npx agentcash discover ${hostname}`,
|
|
1961
|
+
`npx agentcash discover ${hostname} --verbose`
|
|
1962
|
+
],
|
|
1963
|
+
estimatedTokens: l1EstimatedTokens
|
|
1964
|
+
},
|
|
1965
|
+
l2: {
|
|
1966
|
+
layer: "L2",
|
|
1967
|
+
command: `npx agentcash discover ${hostname}`,
|
|
1968
|
+
tokenLight: true,
|
|
1969
|
+
resourceCount: options.result.resources.length,
|
|
1970
|
+
resources: toL2Entries(options.result.resources)
|
|
1971
|
+
},
|
|
1972
|
+
l3: {
|
|
1973
|
+
layer: "L3",
|
|
1974
|
+
command: `npx agentcash discover ${hostname} --verbose`,
|
|
1975
|
+
detailLevel: "endpoint-schema-and-metadata",
|
|
1976
|
+
countsByAuthMode: toAuthModeCount(options.result.resources),
|
|
1977
|
+
countsBySource: toSourceCount(options.result.resources),
|
|
1978
|
+
pricedResourceCount: options.result.resources.filter(
|
|
1979
|
+
(resource) => resource.authHint === "paid"
|
|
1980
|
+
).length
|
|
1981
|
+
},
|
|
1982
|
+
l4: {
|
|
1983
|
+
layer: "L4",
|
|
1984
|
+
guidanceSource: llmsInfo.llmsTxtUrl ? "llms.txt" : "none",
|
|
1985
|
+
llmsTxtUrl: llmsInfo.llmsTxtUrl,
|
|
1986
|
+
llmsTxtTokenEstimate: llmsInfo.llmsTxtTokenEstimate,
|
|
1987
|
+
guidancePreview: llmsInfo.guidancePreview,
|
|
1988
|
+
guidanceStatus: llmsInfo.guidanceStatus
|
|
1989
|
+
},
|
|
1990
|
+
l5: {
|
|
1991
|
+
layer: "L5",
|
|
1992
|
+
status: "out_of_scope",
|
|
1993
|
+
note: "Cross-domain composition is intentionally out of scope for discovery v1."
|
|
1994
|
+
}
|
|
1995
|
+
},
|
|
1996
|
+
diagnostics: {
|
|
1997
|
+
warningCount: options.result.warnings.length,
|
|
1998
|
+
errorWarningCount: options.result.warnings.filter((warning2) => warning2.severity === "error").length,
|
|
1999
|
+
selectedStage: options.result.selectedStage ?? null,
|
|
2000
|
+
upgradeSuggested: options.result.upgradeSuggested,
|
|
2001
|
+
upgradeReasons: options.result.upgradeReasons
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
async function auditContextHarness(options) {
|
|
2006
|
+
const result = await runDiscovery({
|
|
2007
|
+
detailed: true,
|
|
2008
|
+
options: {
|
|
2009
|
+
...options,
|
|
2010
|
+
rawView: "full"
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
return buildContextHarnessReport({
|
|
2014
|
+
target: options.target,
|
|
2015
|
+
result,
|
|
2016
|
+
client: options.client,
|
|
2017
|
+
contextWindowTokens: options.contextWindowTokens
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
function defaultHarnessProbeCandidates(report) {
|
|
2021
|
+
const methodsByPath = /* @__PURE__ */ new Map();
|
|
2022
|
+
for (const resource of report.levels.l2.resources) {
|
|
2023
|
+
const methods = methodsByPath.get(resource.path) ?? /* @__PURE__ */ new Set();
|
|
2024
|
+
methods.add(resource.method);
|
|
2025
|
+
methodsByPath.set(resource.path, methods);
|
|
2026
|
+
}
|
|
2027
|
+
return [...methodsByPath.entries()].map(([path, methods]) => ({
|
|
2028
|
+
path,
|
|
2029
|
+
methods: [...methods]
|
|
2030
|
+
}));
|
|
2031
|
+
}
|
|
2032
|
+
|
|
1400
2033
|
// src/index.ts
|
|
1401
2034
|
async function discover(options) {
|
|
1402
2035
|
return await runDiscovery({ detailed: false, options });
|
|
@@ -1409,7 +2042,14 @@ export {
|
|
|
1409
2042
|
LEGACY_SUNSET_DATE,
|
|
1410
2043
|
STRICT_ESCALATION_CODES,
|
|
1411
2044
|
UPGRADE_WARNING_CODES,
|
|
2045
|
+
auditContextHarness,
|
|
2046
|
+
buildContextHarnessReport,
|
|
1412
2047
|
computeUpgradeSignal,
|
|
2048
|
+
defaultHarnessProbeCandidates,
|
|
1413
2049
|
discover,
|
|
1414
|
-
discoverDetailed
|
|
2050
|
+
discoverDetailed,
|
|
2051
|
+
discoverForMcp,
|
|
2052
|
+
getHarnessClientProfile,
|
|
2053
|
+
inspectEndpointForMcp,
|
|
2054
|
+
listHarnessClientProfiles
|
|
1415
2055
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/discovery",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Canonical OpenAPI-first discovery runtime for the agentcash ecosystem",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
"bin",
|
|
45
45
|
"README.md",
|
|
46
46
|
"LICENSE",
|
|
47
|
-
".claude/CLAUDE.md",
|
|
48
47
|
"AGENTS.md"
|
|
49
48
|
],
|
|
50
49
|
"engines": {
|
|
@@ -54,7 +53,6 @@
|
|
|
54
53
|
"agentcash",
|
|
55
54
|
"discovery",
|
|
56
55
|
"x402",
|
|
57
|
-
"mpp",
|
|
58
56
|
"openapi"
|
|
59
57
|
],
|
|
60
58
|
"license": "MIT",
|
|
@@ -66,6 +64,7 @@
|
|
|
66
64
|
"access": "public"
|
|
67
65
|
},
|
|
68
66
|
"dependencies": {
|
|
67
|
+
"table": "^6.9.0",
|
|
69
68
|
"zod": "^4.1.13"
|
|
70
69
|
},
|
|
71
70
|
"devDependencies": {
|