@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.
Files changed (3) hide show
  1. package/README.md +12 -8
  2. package/dist/index.js +18 -7
  3. 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
- <h1 align="center">CloudCost MCP Server</h1>
3
- <p align="center">
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.
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
- ![CI](https://github.com/jadenrazo/CloudCostMCP/actions/workflows/ci.yml/badge.svg) [![npm version](https://img.shields.io/npm/v/@jadenrazo/cloudcost-mcp.svg)](https://www.npmjs.com/package/@jadenrazo/cloudcost-mcp) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)
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
- for (const [sku, product] of Object.entries(products)) {
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
- for (const [sku, product] of Object.entries(products)) {
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
- for (const [sku, product] of Object.entries(products)) {
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 items) {
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 items[0];
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: byService,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jadenrazo/cloudcost-mcp",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "MCP server for multi-cloud cost analysis of Terraform codebases",
5
5
  "type": "module",
6
6
  "license": "MIT",