@jadenrazo/cloudcost-mcp 0.1.7 → 0.1.8

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 +21 -21
  2. package/dist/index.js +97 -38
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -322,27 +322,27 @@ Configuration priority: environment variables > config file > built-in defaults.
322
322
  │ (src/server.ts) │
323
323
  └──────────────┬────────────────┘
324
324
 
325
- ┌────────────────────────┼─────────────────────────┐
326
- │ │
327
- ┌─────────▼─────────┐ ┌─────────▼─────────┐ ┌─────────▼─────────┐
328
- │ Tool Handlers │ │ HCL Parsers │ │ Cost Engine │
329
- (src/tools/*.ts) │ │ (src/parsers/) │ (src/calculator/) │
330
- └─────────┬─────────┘ └───────────────────┘ └─────────┬─────────┘
331
-
332
- ┌─────────▼──────────────────────────────────────────────────▼─────────┐
333
- │ PricingEngine (router)
334
- (src/pricing/pricing-engine.ts)
335
- └───────┬────────────────────┬──────────────────────┬─────────────────┘
336
-
337
- ┌─────────▼──────┐ ┌─────────▼──────┐ ┌──────────▼──────┐
338
- │ AWS Bulk │ │ Azure Retail │ │ GCP Bundled
339
- │ Loader │ │ Client │ │ Loader
340
- │ (CSV + JSON) │ │ (REST API) │ │ (static JSON)
341
- └───────┬────────┘ └───────┬────────┘ └────────┬────────┘
342
- │ │
343
- ▼ ▼
344
- AWS Bulk Pricing Azure Retail API data/gcp-pricing/
345
- CSV (public) (public, no auth) (bundled files)
325
+ ┌────────────────────────┼───────────────────────┐
326
+ │ │
327
+ ┌─────────▼─────────┐ ┌─────────▼─────────┐ ┌─────────▼─────────┐
328
+ │ Tool Handlers │ │ HCL Parsers │ │ Cost Engine │
329
+ (src/tools/*.ts) │ │ (src/parsers/) │ (src/calculator/) │
330
+ └─────────┬─────────┘ └───────────────────┘ └─────────┬─────────┘
331
+
332
+ ┌─────────▼────────────────────────────────────────────────▼─────────┐
333
+ │ PricingEngine (router)
334
+ (src/pricing/pricing-engine.ts)
335
+ └──────┬───────────────────┬─────────────────────┬───────────────────┘
336
+
337
+ ┌────────▼───────┐ ┌────────▼───────┐ ┌────────▼───────┐
338
+ │ AWS Bulk │ │ Azure Retail │ │ GCP Bundled
339
+ │ Loader │ │ Client │ │ Loader
340
+ │ (CSV + JSON) │ │ (REST API) │ │ (static JSON)
341
+ └────────┬───────┘ └────────┬───────┘ └────────┬───────┘
342
+ │ │
343
+ ▼ ▼
344
+ AWS Bulk Pricing Azure Retail API data/gcp-pricing/
345
+ CSV (public) (public, no auth) (bundled files)
346
346
  ```
347
347
 
348
348
  ### Key Design Decisions
package/dist/index.js CHANGED
@@ -270,17 +270,30 @@ function extractUnit(priceDimensions) {
270
270
  }
271
271
  return "Hrs";
272
272
  }
273
+ function extractFromTerms(onDemandTerms, defaultUnit) {
274
+ for (const skuTerms of Object.values(onDemandTerms)) {
275
+ if (skuTerms?.priceDimensions) {
276
+ return {
277
+ price: extractUsdPrice(skuTerms.priceDimensions),
278
+ unit: extractUnit(skuTerms.priceDimensions)
279
+ };
280
+ }
281
+ for (const offerTerm of Object.values(skuTerms ?? {})) {
282
+ const dims = offerTerm?.priceDimensions;
283
+ if (dims) {
284
+ return {
285
+ price: extractUsdPrice(dims),
286
+ unit: extractUnit(dims)
287
+ };
288
+ }
289
+ }
290
+ }
291
+ return { price: 0, unit: defaultUnit };
292
+ }
273
293
  function normalizeAwsCompute(rawProduct, rawPrice, region) {
274
294
  const attrs = rawProduct?.attributes ?? {};
275
295
  const terms = rawPrice?.terms?.OnDemand ?? {};
276
- let price = 0;
277
- let unit = "Hrs";
278
- for (const term of Object.values(terms)) {
279
- const dims = term?.priceDimensions ?? {};
280
- price = extractUsdPrice(dims);
281
- unit = extractUnit(dims);
282
- break;
283
- }
296
+ const { price, unit } = extractFromTerms(terms, "Hrs");
284
297
  return {
285
298
  provider: "aws",
286
299
  service: "ec2",
@@ -295,7 +308,8 @@ function normalizeAwsCompute(rawProduct, rawPrice, region) {
295
308
  vcpu: attrs.vcpu ?? "",
296
309
  memory: attrs.memory ?? "",
297
310
  operating_system: attrs.operatingSystem ?? "Linux",
298
- tenancy: attrs.tenancy ?? "Shared"
311
+ tenancy: attrs.tenancy ?? "Shared",
312
+ pricing_source: "live"
299
313
  },
300
314
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
301
315
  };
@@ -303,14 +317,7 @@ function normalizeAwsCompute(rawProduct, rawPrice, region) {
303
317
  function normalizeAwsDatabase(rawProduct, rawPrice, region) {
304
318
  const attrs = rawProduct?.attributes ?? {};
305
319
  const terms = rawPrice?.terms?.OnDemand ?? {};
306
- let price = 0;
307
- let unit = "Hrs";
308
- for (const term of Object.values(terms)) {
309
- const dims = term?.priceDimensions ?? {};
310
- price = extractUsdPrice(dims);
311
- unit = extractUnit(dims);
312
- break;
313
- }
320
+ const { price, unit } = extractFromTerms(terms, "Hrs");
314
321
  return {
315
322
  provider: "aws",
316
323
  service: "rds",
@@ -325,7 +332,8 @@ function normalizeAwsDatabase(rawProduct, rawPrice, region) {
325
332
  database_engine: attrs.databaseEngine ?? "",
326
333
  deployment_option: attrs.deploymentOption ?? "Single-AZ",
327
334
  vcpu: attrs.vcpu ?? "",
328
- memory: attrs.memory ?? ""
335
+ memory: attrs.memory ?? "",
336
+ pricing_source: "live"
329
337
  },
330
338
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
331
339
  };
@@ -333,14 +341,7 @@ function normalizeAwsDatabase(rawProduct, rawPrice, region) {
333
341
  function normalizeAwsStorage(rawProduct, rawPrice, region) {
334
342
  const attrs = rawProduct?.attributes ?? {};
335
343
  const terms = rawPrice?.terms?.OnDemand ?? {};
336
- let price = 0;
337
- let unit = "GB-Mo";
338
- for (const term of Object.values(terms)) {
339
- const dims = term?.priceDimensions ?? {};
340
- price = extractUsdPrice(dims);
341
- unit = extractUnit(dims);
342
- break;
343
- }
344
+ const { price, unit } = extractFromTerms(terms, "GB-Mo");
344
345
  const volumeType = attrs.volumeApiName ?? attrs.volumeType ?? "gp3";
345
346
  return {
346
347
  provider: "aws",
@@ -354,7 +355,8 @@ function normalizeAwsStorage(rawProduct, rawPrice, region) {
354
355
  attributes: {
355
356
  volume_type: volumeType,
356
357
  max_iops: attrs.maxIopsvolume ?? "",
357
- max_throughput: attrs.maxThroughputvolume ?? ""
358
+ max_throughput: attrs.maxThroughputvolume ?? "",
359
+ pricing_source: "live"
358
360
  },
359
361
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
360
362
  };
@@ -579,7 +581,7 @@ var AwsBulkLoader = class _AwsBulkLoader {
579
581
  if (afterCsv) return afterCsv;
580
582
  }
581
583
  } catch (err) {
582
- logger.debug("AWS EC2 CSV streaming failed", {
584
+ logger.warn("AWS EC2 CSV streaming failed", {
583
585
  region,
584
586
  instanceType,
585
587
  err: err instanceof Error ? err.message : String(err)
@@ -595,7 +597,7 @@ var AwsBulkLoader = class _AwsBulkLoader {
595
597
  }
596
598
  }
597
599
  } catch (err) {
598
- logger.debug("AWS bulk EC2 JSON fetch failed, using fallback", {
600
+ logger.warn("AWS bulk EC2 JSON fetch failed, using fallback", {
599
601
  region,
600
602
  instanceType,
601
603
  err: err instanceof Error ? err.message : String(err)
@@ -617,7 +619,7 @@ var AwsBulkLoader = class _AwsBulkLoader {
617
619
  }
618
620
  }
619
621
  } catch (err) {
620
- logger.debug("AWS bulk RDS fetch failed, using fallback", {
622
+ logger.warn("AWS bulk RDS fetch failed, using fallback", {
621
623
  region,
622
624
  instanceClass,
623
625
  err: err instanceof Error ? err.message : String(err)
@@ -639,7 +641,7 @@ var AwsBulkLoader = class _AwsBulkLoader {
639
641
  }
640
642
  }
641
643
  } catch (err) {
642
- logger.debug("AWS bulk EBS fetch failed, using fallback", {
644
+ logger.warn("AWS bulk EBS fetch failed, using fallback", {
643
645
  region,
644
646
  volumeType,
645
647
  err: err instanceof Error ? err.message : String(err)
@@ -1296,7 +1298,7 @@ var AzureRetailClient = class {
1296
1298
  return result;
1297
1299
  }
1298
1300
  } catch (err) {
1299
- logger.debug("Azure Retail API VM fetch failed, using fallback", {
1301
+ logger.warn("Azure Retail API VM fetch failed, using fallback", {
1300
1302
  region,
1301
1303
  vmSize,
1302
1304
  err: err instanceof Error ? err.message : String(err)
@@ -1311,15 +1313,18 @@ var AzureRetailClient = class {
1311
1313
  try {
1312
1314
  const armRegion = region.toLowerCase().replace(/\s+/g, "");
1313
1315
  const serviceName = `Azure Database for ${engine}`;
1314
- const filter = this.buildODataFilter(serviceName, armRegion, tier);
1315
- const items = await this.queryPricing(filter);
1316
- if (items.length > 0) {
1317
- const result = normalizeAzureDatabase(items[0]);
1316
+ const result = await this.queryDatabasePrice(tier, armRegion, serviceName);
1317
+ if (result) {
1318
1318
  this.cache.set(cacheKey, result, "azure", "db", region, CACHE_TTL2);
1319
1319
  return result;
1320
1320
  }
1321
+ logger.warn("Azure Retail API returned no matching DB pricing", {
1322
+ region,
1323
+ tier,
1324
+ engine
1325
+ });
1321
1326
  } catch (err) {
1322
- logger.debug("Azure Retail API DB fetch failed, using fallback", {
1327
+ logger.warn("Azure Retail API DB fetch failed, using fallback", {
1323
1328
  region,
1324
1329
  tier,
1325
1330
  err: err instanceof Error ? err.message : String(err)
@@ -1327,6 +1332,56 @@ var AzureRetailClient = class {
1327
1332
  }
1328
1333
  return this.fallbackDatabasePrice(tier, region, engine, cacheKey);
1329
1334
  }
1335
+ /**
1336
+ * Azure DB flexible server tier names follow the pattern:
1337
+ * GP_Standard_D2s_v3 → General Purpose, D-series, 2 vCPUs, v3
1338
+ * MO_Standard_E4ds_v5 → Memory Optimized, E-series, 4 vCPUs, v5
1339
+ *
1340
+ * The Retail API prices DB compute in two ways:
1341
+ * 1. Per-instance entries with armSkuName like "Standard_D2ds_v5" (total price)
1342
+ * 2. Per-vCore entries for a series (e.g., Dsv3Series_Compute) × vCPU count
1343
+ *
1344
+ * This method tries approach 1 first, then falls back to approach 2.
1345
+ */
1346
+ async queryDatabasePrice(tier, armRegion, serviceName) {
1347
+ const vmName = tier.replace(/^(GP|MO|BC|GEN)_/i, "");
1348
+ const exactFilter = `serviceName eq '${serviceName}' and armRegionName eq '${armRegion}' and priceType eq 'Consumption' and armSkuName eq '${vmName}'`;
1349
+ const exactItems = await this.queryPricing(exactFilter);
1350
+ if (exactItems.length > 0) {
1351
+ const match = exactItems.find(
1352
+ (i) => (i.meterName ?? "").toLowerCase().includes("vcore")
1353
+ ) ?? exactItems[0];
1354
+ return normalizeAzureDatabase(match);
1355
+ }
1356
+ const parsed = this.parseVmSeries(vmName);
1357
+ if (!parsed) return null;
1358
+ const tierPrefix = tier.match(/^(GP|MO|BC|GEN)_/i)?.[1]?.toUpperCase();
1359
+ const seriesFamily = tierPrefix === "MO" ? "Memory_Optimized" : "General_Purpose";
1360
+ const seriesFilter = `serviceName eq '${serviceName}' and armRegionName eq '${armRegion}' and priceType eq 'Consumption' and contains(armSkuName, '${seriesFamily}') and contains(armSkuName, '${parsed.series}')`;
1361
+ const seriesItems = await this.queryPricing(seriesFilter);
1362
+ if (seriesItems.length > 0) {
1363
+ const perVcore = seriesItems[0].retailPrice ?? 0;
1364
+ const totalPrice = perVcore * parsed.vcpus;
1365
+ const adjusted = { ...seriesItems[0], retailPrice: totalPrice };
1366
+ return normalizeAzureDatabase(adjusted);
1367
+ }
1368
+ return null;
1369
+ }
1370
+ /**
1371
+ * Parse a VM name like "Standard_D2s_v3" into series info.
1372
+ * Returns { series: "Dsv3", vcpus: 2 } or null if unparseable.
1373
+ */
1374
+ parseVmSeries(vmName) {
1375
+ const match = vmName.match(
1376
+ /Standard_([A-Z])(\d+)([a-z]*)_v(\d+)/i
1377
+ );
1378
+ if (!match) return null;
1379
+ const [, family, vcpuStr, variant, version] = match;
1380
+ const vcpus = parseInt(vcpuStr, 10);
1381
+ if (isNaN(vcpus) || vcpus === 0) return null;
1382
+ const series = `${family}${variant}v${version}`;
1383
+ return { series, vcpus };
1384
+ }
1330
1385
  async getStoragePrice(diskType, region) {
1331
1386
  const cacheKey = this.buildCacheKey("disk", region, diskType);
1332
1387
  const cached = this.cache.get(cacheKey);
@@ -1340,8 +1395,12 @@ var AzureRetailClient = class {
1340
1395
  this.cache.set(cacheKey, result, "azure", "storage", region, CACHE_TTL2);
1341
1396
  return result;
1342
1397
  }
1398
+ logger.warn("Azure Retail API returned no matching storage pricing", {
1399
+ region,
1400
+ diskType
1401
+ });
1343
1402
  } catch (err) {
1344
- logger.debug("Azure Retail API storage fetch failed, using fallback", {
1403
+ logger.warn("Azure Retail API storage fetch failed, using fallback", {
1345
1404
  region,
1346
1405
  diskType,
1347
1406
  err: err instanceof Error ? err.message : String(err)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jadenrazo/cloudcost-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "MCP server for multi-cloud cost analysis of Terraform codebases",
5
5
  "type": "module",
6
6
  "license": "MIT",