@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.
- package/dist/index.js +113 -60
- 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:
|
|
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:
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
provider,
|
|
217
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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,
|
|
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${
|
|
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(
|
|
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
|
|
4586
|
+
version
|
|
4534
4587
|
});
|
|
4535
4588
|
registerTools(server, config);
|
|
4536
4589
|
const transport = new StdioServerTransport();
|