infrawise 0.18.0 → 0.19.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/README.md CHANGED
@@ -176,7 +176,7 @@ Add to your editor's MCP config:
176
176
  | ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
177
177
  | `get_infra_overview` | Complete snapshot — services, counts, high-severity findings, analysis `freshness` (age + stale flag), `configured` flag |
178
178
  | `get_graph_summary` | Full infrastructure graph — all nodes, edges, and findings |
179
- | `get_table_schema` | Column-level schema for named tables/collections — types, PKs, FKs, indexes, DynamoDB keys (no row data) |
179
+ | `get_table_schema` | Column-level schema for named tables/collections — types, PKs, FKs, indexes, DynamoDB keys/billing mode, cost signal (no row data) |
180
180
  | `analyze_function` | Issues in a specific function — scans, missing indexes, N+1, trigger event shapes, missing IAM permissions |
181
181
  | `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
182
182
  | `postgres_index_suggestions` | Exact `CREATE INDEX` SQL for your actual table |
@@ -187,14 +187,14 @@ Add to your editor's MCP config:
187
187
  | `get_topic_details` | SNS topics — subscription counts, protocols, and filter policies (required message attributes per subscription) |
188
188
  | `get_secrets_overview` | Secrets Manager — names, rotation status, and key names inferred from code (values never included) |
189
189
  | `get_parameter_overview` | SSM Parameter Store — names, types, tiers (values never included) |
190
- | `get_lambda_overview` | Lambda functions — runtime, memory, timeout, execution role ARN, triggers (SQS/SNS/DynamoDB/Kinesis/MSK/EventBridge/S3), env var key names |
190
+ | `get_lambda_overview` | Lambda functions — runtime, memory, timeout, execution role ARN, triggers (SQS/SNS/DynamoDB/Kinesis/MSK/EventBridge/S3), env var key names, cost signal |
191
191
  | `get_eventbridge_details` | EventBridge rules — name, state, schedule/event pattern, target functions |
192
192
  | `get_s3_overview` | S3 buckets — versioning, encryption, public access, event notifications |
193
193
  | `get_log_errors` | CloudWatch error patterns and counts (no raw log messages) |
194
194
  | `get_stack_outputs` | Stack outputs and cross-stack exports parsed from local IaC files (Terraform outputs, CFN/CDK Outputs) |
195
195
  | `get_cognito_overview` | Cognito user pools — MFA config, app client auth flows, OAuth settings, token validity (secrets never included) |
196
196
  | `get_stream_details` | Kinesis streams (shards, retention, capacity mode) and MSK clusters (state, Kafka version, brokers) |
197
- | `get_cache_overview` | ElastiCache clusters — engine, encryption in transit/at rest, replication group, failover (data never read) |
197
+ | `get_cache_overview` | ElastiCache clusters — engine, encryption in transit/at rest, replication group, failover, cost signal (data never read) |
198
198
 
199
199
  ---
200
200
 
@@ -408,22 +408,22 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
408
408
 
409
409
  | Service | What it checks |
410
410
  | -------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
411
- | DynamoDB schema | Tables, GSIs, partition keys |
411
+ | DynamoDB schema | Tables, GSIs, partition keys, billing mode, cost signal (provisioned capacity) |
412
412
  | PostgreSQL / MySQL schema | Tables, indexes, column types |
413
413
  | MongoDB schema | Collections, indexes |
414
414
  | SQS | Missing DLQs, unencrypted queues, large backlogs, FIFO detection, visibility timeout vs Lambda timeout mismatch |
415
415
  | SNS | Subscription filter policies — required message attributes per subscription |
416
416
  | Apache Kafka (kafkajs) | Producer/consumer topic mapping from code — any broker (self-hosted, Confluent, Redpanda, MSK); distinct from the MSK Lambda trigger |
417
417
  | Secrets Manager | Missing secret rotation |
418
- | Lambda | Default memory (128 MB), high timeouts, triggers (SQS/SNS/DynamoDB/Kinesis/MSK/EventBridge/S3), missing DLQ on trigger source |
418
+ | Lambda | Default memory (128 MB), high timeouts, triggers (SQS/SNS/DynamoDB/Kinesis/MSK/EventBridge/S3), missing DLQ on trigger source, cost signal (high memory with no throttling evidence) |
419
419
  | S3 | Public access blocking (verify), missing versioning, missing encryption |
420
420
  | EventBridge | Rules, schedules, event patterns, target Lambda functions |
421
421
  | API Gateway | REST, HTTP, and WebSocket APIs — routes, methods, Lambda integrations |
422
- | RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ |
422
+ | RDS | Publicly accessible, no backups, unencrypted, no deletion protection, single-AZ, cost signal (Multi-AZ on a non-production-looking instance) |
423
423
  | CloudWatch Logs | Log groups with no retention policy |
424
424
  | Cognito | User pools and app client config — auth flows, OAuth settings, token validity, client secret presence |
425
425
  | Kinesis / MSK | Streams (shards, retention, capacity mode) and MSK clusters (state, Kafka version, brokers) |
426
- | ElastiCache | Missing in-transit encryption, single-node clusters with no replication |
426
+ | ElastiCache | Missing in-transit encryption, single-node clusters with no replication, cost signal (more than 3 nodes) |
427
427
  | Runtime signals (opt-in) | Lambda throttling/errors and stale queue messages from CloudWatch metrics |
428
428
  | Terraform / CloudFormation / CDK | IaC drift vs deployed state; stack outputs and cross-stack exports |
429
429
 
@@ -499,7 +499,7 @@ src/
499
499
  aws/ DynamoDB, S3, Lambda, SQS/SNS/SSM/Secrets/EventBridge/RDS/APIGateway, CloudWatch
500
500
  db/ PostgreSQL, MySQL, MongoDB
501
501
  iac/ Terraform, CDK, CloudFormation (local file parsing)
502
- analyzers/ 34 rule-based analyzers
502
+ analyzers/ 36 rule-based analyzers
503
503
  context/ Repository scanner (ts-morph AST + Python stdlib-ast subprocess)
504
504
  server/ Fastify MCP server (@modelcontextprotocol/sdk, Streamable HTTP)
505
505
  cli/ CLI commands (Commander.js)
@@ -28,7 +28,15 @@ function parseTableDescription(desc) {
28
28
  indexes.push(lsi.IndexName);
29
29
  }
30
30
  }
31
- return { tableName, partitionKey, sortKey, indexes };
31
+ const billingMode = desc.BillingModeSummary?.BillingMode;
32
+ const provisionedThroughput = desc.ProvisionedThroughput?.ReadCapacityUnits !== undefined &&
33
+ desc.ProvisionedThroughput?.WriteCapacityUnits !== undefined
34
+ ? {
35
+ readCapacityUnits: desc.ProvisionedThroughput.ReadCapacityUnits,
36
+ writeCapacityUnits: desc.ProvisionedThroughput.WriteCapacityUnits,
37
+ }
38
+ : undefined;
39
+ return { tableName, partitionKey, sortKey, indexes, billingMode, provisionedThroughput };
32
40
  }
33
41
  async function listAllTables(client) {
34
42
  const tableNames = [];
@@ -0,0 +1,70 @@
1
+ // ─── Cost signals ────────────────────────────────────────────────────────────
2
+ // No billing API involved — every signal here is derived from resource specs
3
+ // already extracted (config-level heuristics, not real utilization data).
4
+ const HIGH_MEMORY_MB = 3008;
5
+ const NON_PROD_NAME = /dev|staging|test|sandbox/i;
6
+ const MANY_CACHE_NODES = 3;
7
+ export class LambdaHighMemoryAnalyzer {
8
+ name = 'LambdaHighMemoryAnalyzer';
9
+ async analyze(graph) {
10
+ const findings = [];
11
+ for (const node of graph.nodes) {
12
+ if (node.type !== 'lambda')
13
+ continue;
14
+ if ((node.memoryMB ?? 0) < HIGH_MEMORY_MB)
15
+ continue;
16
+ if (node.recentThrottles !== 0)
17
+ continue;
18
+ findings.push({
19
+ severity: 'low',
20
+ issue: `Lambda "${node.name}" has high memory (${node.memoryMB} MB) with zero recent throttles`,
21
+ description: `"${node.name}" is allocated ${node.memoryMB} MB with no recent throttling, which is one signal (not proof) that it may be over-provisioned. AWS Lambda pricing is duration × memory.`,
22
+ recommendation: `Run Lambda Power Tuning on "${node.name}" to find the cost-optimal memory size for its actual workload.`,
23
+ metadata: { functionName: node.name, memoryMB: node.memoryMB },
24
+ });
25
+ }
26
+ return findings;
27
+ }
28
+ }
29
+ export class RDSMultiAZNonProdAnalyzer {
30
+ name = 'RDSMultiAZNonProdAnalyzer';
31
+ async analyze(graph) {
32
+ const findings = [];
33
+ for (const node of graph.nodes) {
34
+ if (node.type !== 'database_instance')
35
+ continue;
36
+ if (!node.multiAZ)
37
+ continue;
38
+ if (!NON_PROD_NAME.test(node.name))
39
+ continue;
40
+ findings.push({
41
+ severity: 'low',
42
+ issue: `RDS instance "${node.name}" has Multi-AZ enabled on what looks like a non-production instance`,
43
+ description: `"${node.name}" has Multi-AZ enabled, which roughly doubles RDS cost by running a standby replica. The name suggests a non-production instance, where Multi-AZ is often unnecessary.`,
44
+ recommendation: `If "${node.name}" is not production, disable Multi-AZ to cut its cost roughly in half.`,
45
+ metadata: { dbInstanceIdentifier: node.name, engine: node.engine },
46
+ });
47
+ }
48
+ return findings;
49
+ }
50
+ }
51
+ /** Advisory only — no Finding, since there's no runtime-signal evidence either way. */
52
+ export function lambdaCostSignal(node) {
53
+ if ((node.memoryMB ?? 0) < HIGH_MEMORY_MB)
54
+ return undefined;
55
+ if (node.recentThrottles !== undefined)
56
+ return undefined; // covered by LambdaHighMemoryAnalyzer's Finding when signals are on
57
+ return `${node.memoryMB} MB memory allocated; verify this matches actual workload needs — Lambda Power Tuning can find the cost-optimal size.`;
58
+ }
59
+ export function dynamoCostSignal(node) {
60
+ if (node.databaseType !== 'dynamodb')
61
+ return undefined;
62
+ if (node.billingMode !== 'PROVISIONED')
63
+ return undefined;
64
+ return 'Provisioned capacity; on-demand may cost less under spiky traffic, provisioned is usually cheaper under steady traffic.';
65
+ }
66
+ export function cacheCostSignal(node) {
67
+ if ((node.numNodes ?? 0) <= MANY_CACHE_NODES)
68
+ return undefined;
69
+ return `${node.numNodes} nodes; verify traffic justifies ${node.numNodes}× the per-node cost.`;
70
+ }
@@ -7,6 +7,7 @@ export { IaCDriftAnalyzer } from './terraform.js';
7
7
  export { PipelineAnalyzer } from './pipeline.js';
8
8
  export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, CacheTransitEncryptionAnalyzer, CacheSingleNodeAnalyzer, LambdaThrottlingAnalyzer, StaleQueueMessagesAnalyzer, } from './aws-services.js';
9
9
  export { RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, } from './rds.js';
10
+ export { LambdaHighMemoryAnalyzer, RDSMultiAZNonProdAnalyzer, lambdaCostSignal, dynamoCostSignal, cacheCostSignal, } from './cost-signals.js';
10
11
  export async function runAllAnalyzers(graph, analyzers) {
11
12
  const allFindings = [];
12
13
  for (const analyzer of analyzers) {
@@ -14,7 +14,7 @@ import { extractRuntimeSignals } from '../../adapters/aws/metrics.js';
14
14
  import { extractS3Metadata } from '../../adapters/aws/s3.js';
15
15
  import { scanRepository } from '../../context/index.js';
16
16
  import { buildGraph, addStackOutputNodes } from '../../graph/index.js';
17
- import { runAllAnalyzers, IaCDriftAnalyzer, PipelineAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, CacheTransitEncryptionAnalyzer, CacheSingleNodeAnalyzer, LambdaThrottlingAnalyzer, StaleQueueMessagesAnalyzer, } from '../../analyzers/index.js';
17
+ import { runAllAnalyzers, IaCDriftAnalyzer, PipelineAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, CacheTransitEncryptionAnalyzer, CacheSingleNodeAnalyzer, LambdaThrottlingAnalyzer, StaleQueueMessagesAnalyzer, LambdaHighMemoryAnalyzer, RDSMultiAZNonProdAnalyzer, } from '../../analyzers/index.js';
18
18
  import { printFinding, printSummaryBox, log, printHeader } from '../utils.js';
19
19
  const SEVERITY_ORDER = { high: 3, medium: 2, low: 1, verify: 0 };
20
20
  function buildMarkdownReport(findings, projectName) {
@@ -102,6 +102,7 @@ function buildAnalyzers(config, iacDriftAnalyzer, iacLambdas) {
102
102
  new LambdaHighTimeoutAnalyzer(),
103
103
  new LambdaMissingTriggerDLQAnalyzer(),
104
104
  new LambdaMissingIAMPermissionsAnalyzer(),
105
+ new LambdaHighMemoryAnalyzer(),
105
106
  ]
106
107
  : []),
107
108
  ...(config.rds?.enabled === true
@@ -111,6 +112,7 @@ function buildAnalyzers(config, iacDriftAnalyzer, iacLambdas) {
111
112
  new RDSUnencryptedAnalyzer(),
112
113
  new RDSNoDeletionProtectionAnalyzer(),
113
114
  new RDSNoMultiAZAnalyzer(),
115
+ new RDSMultiAZNonProdAnalyzer(),
114
116
  ]
115
117
  : []),
116
118
  ...(config.s3?.enabled === true
@@ -20,6 +20,8 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
20
20
  databaseType: 'dynamodb',
21
21
  partitionKey: table.partitionKey,
22
22
  sortKey: table.sortKey,
23
+ billingMode: table.billingMode,
24
+ provisionedThroughput: table.provisionedThroughput,
23
25
  });
24
26
  for (const indexName of table.indexes) {
25
27
  const indexNodeId = `index:${table.tableName}:${indexName}`;
@@ -7,7 +7,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
7
7
  import { z } from 'zod';
8
8
  import { logger } from '../core/index.js';
9
9
  const { version } = JSON.parse(readFileSync(join(import.meta.dirname, '../../package.json'), 'utf8'));
10
- import { summarizeFindings } from '../analyzers/index.js';
10
+ import { summarizeFindings, lambdaCostSignal, dynamoCostSignal, cacheCostSignal, } from '../analyzers/index.js';
11
11
  import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getEventBridgeRuleNodes, getBucketNodes, getAPINodes, getStackOutputNodes, getUserPoolNodes, getStreamNodes, getKafkaClusterNodes, getCacheClusterNodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
12
12
  // ── State ────────────────────────────────────────────────────────────────────
13
13
  let currentGraph = { nodes: [], edges: [] };
@@ -381,7 +381,7 @@ export function createMcpServer() {
381
381
  });
382
382
  }));
383
383
  mcp.registerTool('get_lambda_overview', {
384
- description: 'Returns all Lambda functions with runtime, memory (MB), timeout (sec), environment variable key names (values never returned), and event source triggers with the correct handler event shape for each. Call this when auditing Lambda configuration for default memory (128 MB) or high timeouts, or when you need the trigger event shape for a specific function without running analyze_function. When runtime signals are enabled, recentThrottles and recentErrors report CloudWatch counts for the analysis window.',
384
+ description: 'Returns all Lambda functions with runtime, memory (MB), timeout (sec), environment variable key names (values never returned), and event source triggers with the correct handler event shape for each. Call this when auditing Lambda configuration for default memory (128 MB) or high timeouts, or when you need the trigger event shape for a specific function without running analyze_function. When runtime signals are enabled, recentThrottles and recentErrors report CloudWatch counts for the analysis window. A costSignal note appears when memory is 3008 MB+ and there is no throttling evidence to justify it — no billing API involved, this is a config-level heuristic.',
385
385
  inputSchema: z.object({}),
386
386
  }, logged('get_lambda_overview', async () => {
387
387
  const lambdas = getLambdaNodes(currentGraph);
@@ -389,26 +389,30 @@ export function createMcpServer() {
389
389
  return toText({
390
390
  total: lambdas.length,
391
391
  note: 'Environment variable values are never included.',
392
- lambdas: lambdas.map((l) => ({
393
- name: l.name,
394
- runtime: l.runtime,
395
- memoryMB: l.memoryMB,
396
- timeoutSec: l.timeoutSec,
397
- envVarCount: l.envVarKeys?.length ?? 0,
398
- envVarKeys: l.envVarKeys,
399
- roleArn: l.roleArn,
400
- recentThrottles: l.recentThrottles,
401
- recentErrors: l.recentErrors,
402
- triggers: (l.triggers ?? []).map((t) => ({
403
- type: t.type,
404
- source: t.sourceName,
405
- eventShape: t.eventShape,
406
- state: t.state,
407
- })),
408
- findings: lambdaFindings
409
- .filter((f) => f.metadata.functionName === l.name)
410
- .map((f) => ({ severity: f.severity, issue: f.issue })),
411
- })),
392
+ lambdas: lambdas.map((l) => {
393
+ const costSignal = lambdaCostSignal(l);
394
+ return {
395
+ name: l.name,
396
+ runtime: l.runtime,
397
+ memoryMB: l.memoryMB,
398
+ timeoutSec: l.timeoutSec,
399
+ envVarCount: l.envVarKeys?.length ?? 0,
400
+ envVarKeys: l.envVarKeys,
401
+ roleArn: l.roleArn,
402
+ recentThrottles: l.recentThrottles,
403
+ recentErrors: l.recentErrors,
404
+ ...(costSignal ? { costSignal } : {}),
405
+ triggers: (l.triggers ?? []).map((t) => ({
406
+ type: t.type,
407
+ source: t.sourceName,
408
+ eventShape: t.eventShape,
409
+ state: t.state,
410
+ })),
411
+ findings: lambdaFindings
412
+ .filter((f) => f.metadata.functionName === l.name)
413
+ .map((f) => ({ severity: f.severity, issue: f.issue })),
414
+ };
415
+ }),
412
416
  });
413
417
  }));
414
418
  mcp.registerTool('get_eventbridge_details', {
@@ -489,7 +493,7 @@ export function createMcpServer() {
489
493
  });
490
494
  }));
491
495
  mcp.registerTool('get_table_schema', {
492
- description: 'Returns the full schema for specific tables or collections by name: columns with data types and nullability, primary keys, foreign keys (join paths), indexes, DynamoDB partition/sort keys, and MongoDB estimated document counts. Accepts short names ("orders" matches "public.orders") and is case-insensitive. Call this after get_infra_overview when you need column-level detail to write a SQL query, DynamoDB expression, or MongoDB filter for specific tables — instead of pulling every schema with get_graph_summary. Do NOT call for a table inventory; use get_infra_overview for that. Row data is never included.',
496
+ description: 'Returns the full schema for specific tables or collections by name: columns with data types and nullability, primary keys, foreign keys (join paths), indexes, DynamoDB partition/sort keys and billing mode, and MongoDB estimated document counts. Accepts short names ("orders" matches "public.orders") and is case-insensitive. Call this after get_infra_overview when you need column-level detail to write a SQL query, DynamoDB expression, or MongoDB filter for specific tables — instead of pulling every schema with get_graph_summary. Do NOT call for a table inventory; use get_infra_overview for that. Row data is never included. DynamoDB matches include a costSignal note for provisioned-capacity tables.',
493
497
  inputSchema: z.object({
494
498
  tables: z
495
499
  .array(z.string())
@@ -519,17 +523,25 @@ export function createMcpServer() {
519
523
  return {
520
524
  requested,
521
525
  found: true,
522
- matches: matches.map((t) => ({
523
- name: t.name,
524
- databaseType: t.databaseType,
525
- ...(t.columns ? { columns: t.columns } : {}),
526
- ...(t.primaryKeys?.length ? { primaryKeys: t.primaryKeys } : {}),
527
- ...(t.foreignKeys?.length ? { foreignKeys: t.foreignKeys } : {}),
528
- ...(t.partitionKey ? { partitionKey: t.partitionKey } : {}),
529
- ...(t.sortKey ? { sortKey: t.sortKey } : {}),
530
- ...(t.estimatedCount !== undefined ? { estimatedCount: t.estimatedCount } : {}),
531
- indexes: indexNamesFor(t.id),
532
- })),
526
+ matches: matches.map((t) => {
527
+ const costSignal = t.databaseType === 'dynamodb' ? dynamoCostSignal(t) : undefined;
528
+ return {
529
+ name: t.name,
530
+ databaseType: t.databaseType,
531
+ ...(t.columns ? { columns: t.columns } : {}),
532
+ ...(t.primaryKeys?.length ? { primaryKeys: t.primaryKeys } : {}),
533
+ ...(t.foreignKeys?.length ? { foreignKeys: t.foreignKeys } : {}),
534
+ ...(t.partitionKey ? { partitionKey: t.partitionKey } : {}),
535
+ ...(t.sortKey ? { sortKey: t.sortKey } : {}),
536
+ ...(t.estimatedCount !== undefined ? { estimatedCount: t.estimatedCount } : {}),
537
+ ...(t.billingMode ? { billingMode: t.billingMode } : {}),
538
+ ...(t.provisionedThroughput
539
+ ? { provisionedThroughput: t.provisionedThroughput }
540
+ : {}),
541
+ ...(costSignal ? { costSignal } : {}),
542
+ indexes: indexNamesFor(t.id),
543
+ };
544
+ }),
533
545
  };
534
546
  });
535
547
  return toText({
@@ -538,7 +550,7 @@ export function createMcpServer() {
538
550
  });
539
551
  }));
540
552
  mcp.registerTool('get_cache_overview', {
541
- description: 'Returns all ElastiCache clusters with engine, version, node type, node count, in-transit and at-rest encryption status, replication group, and automatic failover state. Call this before writing cache client code (TLS is required when transit encryption is on — rediss:// for Redis) or when reviewing cache availability and security posture. Cached data is never read or included.',
553
+ description: 'Returns all ElastiCache clusters with engine, version, node type, node count, in-transit and at-rest encryption status, replication group, and automatic failover state. Call this before writing cache client code (TLS is required when transit encryption is on — rediss:// for Redis) or when reviewing cache availability and security posture. Cached data is never read or included. A costSignal note appears on clusters with more than 3 nodes.',
542
554
  inputSchema: z.object({}),
543
555
  }, logged('get_cache_overview', async () => {
544
556
  const caches = getCacheClusterNodes(currentGraph);
@@ -546,20 +558,24 @@ export function createMcpServer() {
546
558
  return toText({
547
559
  total: caches.length,
548
560
  note: 'Cached data is never included.',
549
- caches: caches.map((c) => ({
550
- id: c.name,
551
- engine: c.engine,
552
- engineVersion: c.engineVersion,
553
- nodeType: c.nodeType,
554
- numNodes: c.numNodes,
555
- transitEncryption: c.transitEncryption,
556
- atRestEncryption: c.atRestEncryption,
557
- replicationGroupId: c.replicationGroupId,
558
- automaticFailover: c.automaticFailover,
559
- findings: cacheFindings
560
- .filter((f) => f.metadata.cacheClusterId === c.name)
561
- .map((f) => ({ severity: f.severity, issue: f.issue })),
562
- })),
561
+ caches: caches.map((c) => {
562
+ const costSignal = cacheCostSignal(c);
563
+ return {
564
+ id: c.name,
565
+ engine: c.engine,
566
+ engineVersion: c.engineVersion,
567
+ nodeType: c.nodeType,
568
+ numNodes: c.numNodes,
569
+ transitEncryption: c.transitEncryption,
570
+ atRestEncryption: c.atRestEncryption,
571
+ replicationGroupId: c.replicationGroupId,
572
+ automaticFailover: c.automaticFailover,
573
+ ...(costSignal ? { costSignal } : {}),
574
+ findings: cacheFindings
575
+ .filter((f) => f.metadata.cacheClusterId === c.name)
576
+ .map((f) => ({ severity: f.severity, issue: f.issue })),
577
+ };
578
+ }),
563
579
  });
564
580
  }));
565
581
  mcp.registerTool('get_stream_details', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infrawise",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "mcpName": "io.github.Sidd27/infrawise",
5
5
  "description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, S3, API Gateway, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
6
6
  "keywords": [