@jadenrazo/cloudcost-mcp 0.1.9 → 0.2.0

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 (2) hide show
  1. package/dist/index.js +113 -60
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  } from "./chunk-KZJSZMWM.js";
15
15
 
16
16
  // src/server.ts
17
+ import { createRequire } from "module";
17
18
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
19
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
20
 
@@ -55,8 +56,9 @@ function loadEnvConfig() {
55
56
  const config = {};
56
57
  const env = process.env;
57
58
  if (env.CLOUDCOST_CACHE_TTL || env.CLOUDCOST_CACHE_PATH) {
59
+ const parsedTtl = env.CLOUDCOST_CACHE_TTL ? parseInt(env.CLOUDCOST_CACHE_TTL, 10) : NaN;
58
60
  config.cache = {
59
- ttl_seconds: env.CLOUDCOST_CACHE_TTL ? parseInt(env.CLOUDCOST_CACHE_TTL, 10) : DEFAULT_CONFIG.cache.ttl_seconds,
61
+ ttl_seconds: !isNaN(parsedTtl) ? parsedTtl : DEFAULT_CONFIG.cache.ttl_seconds,
60
62
  db_path: env.CLOUDCOST_CACHE_PATH ?? ""
61
63
  };
62
64
  }
@@ -66,9 +68,10 @@ function loadEnvConfig() {
66
68
  };
67
69
  }
68
70
  if (env.CLOUDCOST_MONTHLY_HOURS) {
71
+ const parsedHours = parseInt(env.CLOUDCOST_MONTHLY_HOURS, 10);
69
72
  config.pricing = {
70
73
  ...DEFAULT_CONFIG.pricing,
71
- monthly_hours: parseInt(env.CLOUDCOST_MONTHLY_HOURS, 10)
74
+ monthly_hours: !isNaN(parsedHours) ? parsedHours : DEFAULT_CONFIG.pricing.monthly_hours
72
75
  };
73
76
  }
74
77
  return config;
@@ -157,18 +160,26 @@ var PricingCache = class {
157
160
  * table self-trims on read.
158
161
  */
159
162
  get(key) {
160
- const row = this.db.prepare(
161
- "SELECT * FROM pricing_cache WHERE key = ?"
162
- ).get(key);
163
- if (!row) return null;
164
- const now = (/* @__PURE__ */ new Date()).toISOString();
165
- if (row.expires_at <= now) {
166
- this.db.prepare("DELETE FROM pricing_cache WHERE key = ?").run(key);
167
- logger.debug("Cache miss (expired)", { key });
163
+ try {
164
+ const row = this.db.prepare(
165
+ "SELECT * FROM pricing_cache WHERE key = ?"
166
+ ).get(key);
167
+ if (!row) return null;
168
+ const now = (/* @__PURE__ */ new Date()).toISOString();
169
+ if (row.expires_at <= now) {
170
+ this.db.prepare("DELETE FROM pricing_cache WHERE key = ?").run(key);
171
+ logger.debug("Cache miss (expired)", { key });
172
+ return null;
173
+ }
174
+ logger.debug("Cache hit", { key });
175
+ return JSON.parse(row.data);
176
+ } catch (err) {
177
+ logger.warn("Cache get failed, treating as miss", {
178
+ key,
179
+ err: err instanceof Error ? err.message : String(err)
180
+ });
168
181
  return null;
169
182
  }
170
- logger.debug("Cache hit", { key });
171
- return JSON.parse(row.data);
172
183
  }
173
184
  /**
174
185
  * Store a value in the cache.
@@ -181,10 +192,11 @@ var PricingCache = class {
181
192
  * be reused for other cache domains in the future.
182
193
  */
183
194
  set(key, data, provider, service, region, ttlSeconds) {
184
- const now = /* @__PURE__ */ new Date();
185
- const expiresAt = new Date(now.getTime() + ttlSeconds * 1e3);
186
- this.db.prepare(
187
- `INSERT INTO pricing_cache (key, data, provider, service, region, created_at, expires_at)
195
+ try {
196
+ const now = /* @__PURE__ */ new Date();
197
+ const expiresAt = new Date(now.getTime() + ttlSeconds * 1e3);
198
+ this.db.prepare(
199
+ `INSERT INTO pricing_cache (key, data, provider, service, region, created_at, expires_at)
188
200
  VALUES (?, ?, ?, ?, ?, ?, ?)
189
201
  ON CONFLICT (key) DO UPDATE SET
190
202
  data = excluded.data,
@@ -193,58 +205,92 @@ var PricingCache = class {
193
205
  region = excluded.region,
194
206
  created_at = excluded.created_at,
195
207
  expires_at = excluded.expires_at`
196
- ).run(
197
- key,
198
- JSON.stringify(data),
199
- provider,
200
- service,
201
- region,
202
- now.toISOString(),
203
- expiresAt.toISOString()
204
- );
205
- logger.debug("Cache set", { key, ttlSeconds });
208
+ ).run(
209
+ key,
210
+ JSON.stringify(data),
211
+ provider,
212
+ service,
213
+ region,
214
+ now.toISOString(),
215
+ expiresAt.toISOString()
216
+ );
217
+ logger.debug("Cache set", { key, ttlSeconds });
218
+ } catch (err) {
219
+ logger.warn("Cache set failed, continuing without caching", {
220
+ key,
221
+ err: err instanceof Error ? err.message : String(err)
222
+ });
223
+ }
206
224
  }
207
225
  /** Remove a single cache entry. */
208
226
  invalidate(key) {
209
- const result = this.db.prepare("DELETE FROM pricing_cache WHERE key = ?").run(key);
210
- logger.debug("Cache invalidated", { key, removed: result.changes });
227
+ try {
228
+ const result = this.db.prepare("DELETE FROM pricing_cache WHERE key = ?").run(key);
229
+ logger.debug("Cache invalidated", { key, removed: result.changes });
230
+ } catch (err) {
231
+ logger.warn("Cache invalidate failed", {
232
+ key,
233
+ err: err instanceof Error ? err.message : String(err)
234
+ });
235
+ }
211
236
  }
212
237
  /** Remove every cache entry belonging to a provider. */
213
238
  invalidateByProvider(provider) {
214
- const result = this.db.prepare("DELETE FROM pricing_cache WHERE provider = ?").run(provider);
215
- logger.debug("Cache invalidated by provider", {
216
- provider,
217
- removed: result.changes
218
- });
239
+ try {
240
+ const result = this.db.prepare("DELETE FROM pricing_cache WHERE provider = ?").run(provider);
241
+ logger.debug("Cache invalidated by provider", {
242
+ provider,
243
+ removed: result.changes
244
+ });
245
+ } catch (err) {
246
+ logger.warn("Cache invalidateByProvider failed", {
247
+ provider,
248
+ err: err instanceof Error ? err.message : String(err)
249
+ });
250
+ }
219
251
  }
220
252
  /**
221
253
  * Delete all entries whose expiry timestamp is in the past.
222
254
  * Returns the number of rows removed.
223
255
  */
224
256
  cleanup() {
225
- const now = (/* @__PURE__ */ new Date()).toISOString();
226
- const result = this.db.prepare("DELETE FROM pricing_cache WHERE expires_at <= ?").run(now);
227
- const removed = result.changes;
228
- logger.debug("Cache cleanup complete", { removed });
229
- return removed;
257
+ try {
258
+ const now = (/* @__PURE__ */ new Date()).toISOString();
259
+ const result = this.db.prepare("DELETE FROM pricing_cache WHERE expires_at <= ?").run(now);
260
+ const removed = result.changes;
261
+ logger.debug("Cache cleanup complete", { removed });
262
+ return removed;
263
+ } catch (err) {
264
+ logger.warn("Cache cleanup failed", {
265
+ err: err instanceof Error ? err.message : String(err)
266
+ });
267
+ return 0;
268
+ }
230
269
  }
231
270
  /** Return aggregate statistics about the current cache state. */
232
271
  getStats() {
233
- const now = (/* @__PURE__ */ new Date()).toISOString();
234
- const totals = this.db.prepare(
235
- `SELECT
272
+ try {
273
+ const now = (/* @__PURE__ */ new Date()).toISOString();
274
+ const totals = this.db.prepare(
275
+ `SELECT
236
276
  COUNT(*) AS total_entries,
237
277
  COALESCE(SUM(LENGTH(data)), 0) AS size_bytes
238
278
  FROM pricing_cache`
239
- ).get();
240
- const expired = this.db.prepare(
241
- "SELECT COUNT(*) AS expired_entries FROM pricing_cache WHERE expires_at <= ?"
242
- ).get(now);
243
- return {
244
- total_entries: totals.total_entries,
245
- expired_entries: expired.expired_entries,
246
- size_bytes: totals.size_bytes
247
- };
279
+ ).get();
280
+ const expired = this.db.prepare(
281
+ "SELECT COUNT(*) AS expired_entries FROM pricing_cache WHERE expires_at <= ?"
282
+ ).get(now);
283
+ return {
284
+ total_entries: totals.total_entries,
285
+ expired_entries: expired.expired_entries,
286
+ size_bytes: totals.size_bytes
287
+ };
288
+ } catch (err) {
289
+ logger.warn("Cache getStats failed", {
290
+ err: err instanceof Error ? err.message : String(err)
291
+ });
292
+ return { total_entries: 0, expired_entries: 0, size_bytes: 0 };
293
+ }
248
294
  }
249
295
  /** Close the underlying database connection. */
250
296
  close() {
@@ -1127,7 +1173,7 @@ function normalizeAzureCompute(item) {
1127
1173
  service_name: item.serviceName ?? "Virtual Machines",
1128
1174
  product_name: item.productName ?? "",
1129
1175
  meter_name: item.meterName ?? "",
1130
- pricing_source: "azure-retail-api"
1176
+ pricing_source: "live"
1131
1177
  },
1132
1178
  effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1133
1179
  };
@@ -1147,7 +1193,7 @@ function normalizeAzureDatabase(item) {
1147
1193
  service_name: item.serviceName ?? "",
1148
1194
  product_name: item.productName ?? "",
1149
1195
  meter_name: item.meterName ?? "",
1150
- pricing_source: "azure-retail-api"
1196
+ pricing_source: "live"
1151
1197
  },
1152
1198
  effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1153
1199
  };
@@ -1167,7 +1213,7 @@ function normalizeAzureStorage(item) {
1167
1213
  service_name: item.serviceName ?? "",
1168
1214
  product_name: item.productName ?? "",
1169
1215
  meter_name: item.meterName ?? "",
1170
- pricing_source: "azure-retail-api"
1216
+ pricing_source: "live"
1171
1217
  },
1172
1218
  effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1173
1219
  };
@@ -1381,10 +1427,10 @@ var AzureRetailClient = class {
1381
1427
  /Standard_([A-Z])(\d+)([a-z]*)_v(\d+)/i
1382
1428
  );
1383
1429
  if (!match) return null;
1384
- const [, family, vcpuStr, variant, version] = match;
1430
+ const [, family, vcpuStr, variant, version2] = match;
1385
1431
  const vcpus = parseInt(vcpuStr, 10);
1386
1432
  if (isNaN(vcpus) || vcpus === 0) return null;
1387
- const series = `${family}${variant}v${version}`;
1433
+ const series = `${family}${variant}v${version2}`;
1388
1434
  return { series, vcpus };
1389
1435
  }
1390
1436
  async getStoragePrice(diskType, region) {
@@ -2004,8 +2050,11 @@ var GcpProvider = class {
2004
2050
  getNatGatewayPrice(region) {
2005
2051
  return this.loader.getNatGatewayPrice(region);
2006
2052
  }
2007
- getKubernetesPrice(region) {
2008
- return this.loader.getKubernetesPrice(region);
2053
+ getKubernetesPrice(region, mode) {
2054
+ return this.loader.getKubernetesPrice(
2055
+ region,
2056
+ mode === "autopilot" ? "autopilot" : "standard"
2057
+ );
2009
2058
  }
2010
2059
  };
2011
2060
  var PricingEngine = class {
@@ -2065,7 +2114,7 @@ var PricingEngine = class {
2065
2114
  return p.getNatGatewayPrice(region);
2066
2115
  }
2067
2116
  if (svc === "k8s" || svc === "kubernetes" || svc === "eks" || svc === "aks" || svc === "gke") {
2068
- return p.getKubernetesPrice(region);
2117
+ return p.getKubernetesPrice(region, attributes.mode);
2069
2118
  }
2070
2119
  logger.warn("PricingEngine: unrecognised service", { service, provider });
2071
2120
  return null;
@@ -4374,7 +4423,7 @@ async function getEquivalents(params) {
4374
4423
  import { z as z5 } from "zod";
4375
4424
  var getPricingSchema = z5.object({
4376
4425
  provider: z5.enum(["aws", "azure", "gcp"]).describe("Cloud provider to look up pricing for"),
4377
- service: z5.enum(["compute", "database", "storage", "network", "kubernetes"]).describe("Service category"),
4426
+ service: z5.enum(["compute", "database", "storage", "network", "load_balancer", "nat_gateway", "kubernetes"]).describe("Service category (network defaults to nat_gateway for backward compatibility)"),
4378
4427
  resource_type: z5.string().describe(
4379
4428
  "Instance type, storage type, or resource identifier (e.g. t3.large, gp3, Standard_D4s_v3)"
4380
4429
  ),
@@ -4387,6 +4436,8 @@ async function getPricing(params, pricingEngine) {
4387
4436
  database: "database",
4388
4437
  storage: "storage",
4389
4438
  network: "nat-gateway",
4439
+ load_balancer: "load-balancer",
4440
+ nat_gateway: "nat-gateway",
4390
4441
  kubernetes: "kubernetes"
4391
4442
  };
4392
4443
  const service = serviceMap[params.service] ?? params.service;
@@ -4525,12 +4576,14 @@ function registerTools(server, config) {
4525
4576
  }
4526
4577
 
4527
4578
  // src/server.ts
4579
+ var require2 = createRequire(import.meta.url);
4580
+ var { version } = require2("../package.json");
4528
4581
  async function startServer() {
4529
4582
  const config = loadConfig();
4530
4583
  setLogLevel(config.logging.level);
4531
4584
  const server = new McpServer({
4532
4585
  name: "cloudcost-mcp",
4533
- version: "0.1.0"
4586
+ version
4534
4587
  });
4535
4588
  registerTools(server, config);
4536
4589
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jadenrazo/cloudcost-mcp",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for multi-cloud cost analysis of Terraform codebases",
5
5
  "type": "module",
6
6
  "license": "MIT",