@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.
- package/README.md +21 -21
- package/dist/index.js +97 -38
- 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 │
|
|
329
|
-
│
|
|
330
|
-
└─────────┬─────────┘
|
|
331
|
-
│
|
|
332
|
-
|
|
333
|
-
│ PricingEngine (router)
|
|
334
|
-
│
|
|
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
|
|
345
|
-
CSV (public) (public, no auth)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
1315
|
-
|
|
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.
|
|
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.
|
|
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)
|