@jadenrazo/cloudcost-mcp 0.1.8 → 0.1.9
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 +12 -8
- package/dist/index.js +18 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
<h1 align="center">CloudCost MCP Server</h1>
|
|
2
|
+
|
|
1
3
|
<p align="center">
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
<br />
|
|
6
|
-
Built on the <a href="https://modelcontextprotocol.io">Model Context Protocol</a> for seamless AI agent integration.
|
|
7
|
-
</p>
|
|
4
|
+
Multi-cloud cost analysis for Terraform — powered by live pricing data from AWS, Azure, and GCP.
|
|
5
|
+
<br />
|
|
6
|
+
Built on the <a href="https://modelcontextprotocol.io">Model Context Protocol</a> for seamless AI agent integration.
|
|
8
7
|
</p>
|
|
9
8
|
|
|
10
9
|
<p align="center">
|
|
10
|
+
<a href="https://github.com/jadenrazo/CloudCostMCP/actions/workflows/ci.yml"><img src="https://github.com/jadenrazo/CloudCostMCP/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@jadenrazo/cloudcost-mcp"><img src="https://img.shields.io/npm/v/@jadenrazo/cloudcost-mcp.svg" alt="npm version" /></a>
|
|
12
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
13
|
+
<img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="Node.js" />
|
|
14
|
+
</p>
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
<p align="right">
|
|
17
|
+
<img src="https://github.com/user-attachments/assets/7d5f613a-851e-4480-900f-438d13f9a56e" alt="CloudCost MCP demo" width="700" />
|
|
14
18
|
</p>
|
|
15
19
|
|
|
16
20
|
<p align="center">
|
package/dist/index.js
CHANGED
|
@@ -920,7 +920,8 @@ var AwsBulkLoader = class _AwsBulkLoader {
|
|
|
920
920
|
extractEc2Price(bulk, instanceType, region, os) {
|
|
921
921
|
const products = bulk?.products ?? {};
|
|
922
922
|
const onDemand = bulk?.terms?.OnDemand ?? {};
|
|
923
|
-
|
|
923
|
+
const sortedEntries = Object.entries(products).sort(([a], [b]) => a.localeCompare(b));
|
|
924
|
+
for (const [sku, product] of sortedEntries) {
|
|
924
925
|
const attrs = product?.attributes ?? {};
|
|
925
926
|
if (attrs.instanceType === instanceType && (attrs.operatingSystem ?? "").toLowerCase() === os.toLowerCase() && attrs.tenancy === "Shared" && attrs.capacitystatus === "Used") {
|
|
926
927
|
const priceTerms = onDemand[sku];
|
|
@@ -934,7 +935,8 @@ var AwsBulkLoader = class _AwsBulkLoader {
|
|
|
934
935
|
extractRdsPrice(bulk, instanceClass, region, engine) {
|
|
935
936
|
const products = bulk?.products ?? {};
|
|
936
937
|
const onDemand = bulk?.terms?.OnDemand ?? {};
|
|
937
|
-
|
|
938
|
+
const sortedEntries = Object.entries(products).sort(([a], [b]) => a.localeCompare(b));
|
|
939
|
+
for (const [sku, product] of sortedEntries) {
|
|
938
940
|
const attrs = product?.attributes ?? {};
|
|
939
941
|
if (attrs.instanceType === instanceClass && (attrs.databaseEngine ?? "").toLowerCase().includes(engine.toLowerCase()) && attrs.deploymentOption === "Single-AZ") {
|
|
940
942
|
const priceTerms = onDemand[sku];
|
|
@@ -948,7 +950,8 @@ var AwsBulkLoader = class _AwsBulkLoader {
|
|
|
948
950
|
extractEbsPrice(bulk, volumeType, region) {
|
|
949
951
|
const products = bulk?.products ?? {};
|
|
950
952
|
const onDemand = bulk?.terms?.OnDemand ?? {};
|
|
951
|
-
|
|
953
|
+
const sortedEntries = Object.entries(products).sort(([a], [b]) => a.localeCompare(b));
|
|
954
|
+
for (const [sku, product] of sortedEntries) {
|
|
952
955
|
const attrs = product?.attributes ?? {};
|
|
953
956
|
const apiName = (attrs.volumeApiName ?? "").toLowerCase();
|
|
954
957
|
if (product?.productFamily === "Storage" && (apiName === volumeType.toLowerCase() || (attrs.volumeType ?? "").toLowerCase() === volumeType.toLowerCase())) {
|
|
@@ -1348,6 +1351,7 @@ var AzureRetailClient = class {
|
|
|
1348
1351
|
const exactFilter = `serviceName eq '${serviceName}' and armRegionName eq '${armRegion}' and priceType eq 'Consumption' and armSkuName eq '${vmName}'`;
|
|
1349
1352
|
const exactItems = await this.queryPricing(exactFilter);
|
|
1350
1353
|
if (exactItems.length > 0) {
|
|
1354
|
+
exactItems.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
|
|
1351
1355
|
const match = exactItems.find(
|
|
1352
1356
|
(i) => (i.meterName ?? "").toLowerCase().includes("vcore")
|
|
1353
1357
|
) ?? exactItems[0];
|
|
@@ -1360,6 +1364,7 @@ var AzureRetailClient = class {
|
|
|
1360
1364
|
const seriesFilter = `serviceName eq '${serviceName}' and armRegionName eq '${armRegion}' and priceType eq 'Consumption' and contains(armSkuName, '${seriesFamily}') and contains(armSkuName, '${parsed.series}')`;
|
|
1361
1365
|
const seriesItems = await this.queryPricing(seriesFilter);
|
|
1362
1366
|
if (seriesItems.length > 0) {
|
|
1367
|
+
seriesItems.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
|
|
1363
1368
|
const perVcore = seriesItems[0].retailPrice ?? 0;
|
|
1364
1369
|
const totalPrice = perVcore * parsed.vcpus;
|
|
1365
1370
|
const adjusted = { ...seriesItems[0], retailPrice: totalPrice };
|
|
@@ -1391,6 +1396,7 @@ var AzureRetailClient = class {
|
|
|
1391
1396
|
const filter = this.buildODataFilter("Storage", armRegion, diskType);
|
|
1392
1397
|
const items = await this.queryPricing(filter);
|
|
1393
1398
|
if (items.length > 0) {
|
|
1399
|
+
items.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
|
|
1394
1400
|
const result = normalizeAzureStorage(items[0]);
|
|
1395
1401
|
this.cache.set(cacheKey, result, "azure", "storage", region, CACHE_TTL2);
|
|
1396
1402
|
return result;
|
|
@@ -1501,16 +1507,19 @@ var AzureRetailClient = class {
|
|
|
1501
1507
|
*/
|
|
1502
1508
|
pickVmItem(items, vmSize, os) {
|
|
1503
1509
|
if (items.length === 0) return null;
|
|
1510
|
+
const sorted = [...items].sort(
|
|
1511
|
+
(a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? "")
|
|
1512
|
+
);
|
|
1504
1513
|
const vmLower = vmSize.toLowerCase();
|
|
1505
1514
|
const isWindows = os.toLowerCase().includes("windows");
|
|
1506
|
-
for (const item of
|
|
1515
|
+
for (const item of sorted) {
|
|
1507
1516
|
const skuLower = (item.skuName ?? "").toLowerCase();
|
|
1508
1517
|
const hasWindows = skuLower.includes("windows");
|
|
1509
1518
|
if (skuLower.includes(vmLower) && (isWindows && hasWindows || !isWindows && !hasWindows)) {
|
|
1510
1519
|
return item;
|
|
1511
1520
|
}
|
|
1512
1521
|
}
|
|
1513
|
-
return
|
|
1522
|
+
return sorted[0];
|
|
1514
1523
|
}
|
|
1515
1524
|
// -------------------------------------------------------------------------
|
|
1516
1525
|
// Internal helpers – size interpolation
|
|
@@ -2979,7 +2988,7 @@ function findNearestInstance(spec, targetProvider) {
|
|
|
2979
2988
|
if (candidate.category === spec.category) {
|
|
2980
2989
|
score += SCORE_CATEGORY;
|
|
2981
2990
|
}
|
|
2982
|
-
if (score > bestScore) {
|
|
2991
|
+
if (score > bestScore || score === bestScore && bestType !== null && candidate.instance_type < bestType) {
|
|
2983
2992
|
bestScore = score;
|
|
2984
2993
|
bestType = candidate.instance_type;
|
|
2985
2994
|
}
|
|
@@ -3716,7 +3725,9 @@ var CostEngine = class {
|
|
|
3716
3725
|
total_monthly: Math.round(totalMonthly * 100) / 100,
|
|
3717
3726
|
total_yearly: Math.round(totalMonthly * 12 * 100) / 100,
|
|
3718
3727
|
currency: "USD",
|
|
3719
|
-
by_service:
|
|
3728
|
+
by_service: Object.fromEntries(
|
|
3729
|
+
Object.entries(byService).map(([k, v]) => [k, Math.round(v * 100) / 100])
|
|
3730
|
+
),
|
|
3720
3731
|
by_resource: estimates,
|
|
3721
3732
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3722
3733
|
warnings
|