infrawise 0.4.2 → 0.5.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
@@ -1,9 +1,11 @@
1
1
  # Infrawise
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/infrawise)](https://www.npmjs.com/package/infrawise)
4
- [![Publish to npm](https://github.com/Sidd27/infrawise/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/Sidd27/infrawise/actions/workflows/npm-publish.yml)
5
- [![CI](https://github.com/Sidd27/infrawise/actions/workflows/ci.yml/badge.svg)](https://github.com/Sidd27/infrawise/actions/workflows/ci.yml)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
3
+ <table><tr>
4
+ <td><a href="https://www.npmjs.com/package/infrawise"><img src="https://img.shields.io/npm/v/infrawise" alt="npm version"></a></td>
5
+ <td><a href="https://github.com/Sidd27/infrawise/actions/workflows/npm-publish.yml"><img src="https://github.com/Sidd27/infrawise/actions/workflows/npm-publish.yml/badge.svg" alt="Publish to npm"></a></td>
6
+ <td><a href="https://github.com/Sidd27/infrawise/actions/workflows/ci.yml"><img src="https://github.com/Sidd27/infrawise/actions/workflows/ci.yml/badge.svg" alt="CI"></a></td>
7
+ <td><a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a></td>
8
+ </tr></table>
7
9
 
8
10
  **Understand your infrastructure, not just your code.**
9
11
 
@@ -78,6 +80,8 @@ infrawise doctor
78
80
  infrawise analyze
79
81
  ```
80
82
 
83
+ Or skip this step — `infrawise dev` auto-runs analysis if no cache exists.
84
+
81
85
  ```
82
86
  Findings (3 total)
83
87
 
@@ -104,11 +108,22 @@ infrawise dev
104
108
  ```
105
109
 
106
110
  ```
107
- Tool server running
108
- Context engine initialized
109
-
110
- MCP endpoint: http://localhost:3000/mcp
111
- Available tools: http://localhost:3000/mcp/tools
111
+ Config loaded infrawise.yaml
112
+ Cached analysis loaded 42 nodes · 18 edges · 7 finding(s)
113
+ ✔ Server running
114
+
115
+ ┌────────────────────────────────────────────────────┐
116
+ │ MCP Server │
117
+ ├────────────────────────────────────────────────────┤
118
+ │ POST http://localhost:3000/mcp │
119
+ │ GET http://localhost:3000/health │
120
+ ├────────────────────────────────────────────────────┤
121
+ │ Tools (13 active) │
122
+ │ get_infra_overview · get_graph_summary │
123
+ │ ... │
124
+ └────────────────────────────────────────────────────┘
125
+
126
+ Watching for file changes... Press Ctrl+C to stop
112
127
  ```
113
128
 
114
129
  ### Step 2: Add to your editor settings
@@ -160,41 +175,6 @@ To let Claude Code manage the server lifecycle automatically:
160
175
 
161
176
  ---
162
177
 
163
- ## GitHub Action
164
-
165
- Add infrastructure analysis to any PR workflow. No `infrawise.yaml` required — the action auto-generates one from your AWS credentials.
166
-
167
- ```yaml
168
- # .github/workflows/infrawise.yml
169
- name: Infrawise
170
- on: [pull_request]
171
-
172
- jobs:
173
- analyze:
174
- runs-on: ubuntu-latest
175
- steps:
176
- - uses: actions/checkout@v4
177
- - uses: Sidd27/infrawise@v0
178
- with:
179
- fail-on: high # fail the PR on high severity findings (default)
180
- env:
181
- AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
182
- AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
183
- AWS_REGION: ${{ secrets.AWS_REGION }}
184
- ```
185
-
186
- If `infrawise.yaml` is not present in the repo, the action generates a minimal config with DynamoDB, Lambda, SQS, SNS, SSM, Secrets Manager, and RDS enabled — using `AWS_REGION` from the environment. Add an `infrawise.yaml` to your repo to customize which services to include or to add database connections.
187
-
188
- Findings appear as annotations on the PR diff and in the job summary.
189
-
190
- | Input | Default | Description |
191
- |---|---|---|
192
- | `version` | `latest` | infrawise version to install |
193
- | `fail-on` | `high` | Exit code 1 if findings at or above this severity (`high`, `medium`, `low`, `never`) |
194
- | `config` | `infrawise.yaml` | Path to config file — auto-generated if not present |
195
-
196
- ---
197
-
198
178
  ## CLI reference
199
179
 
200
180
  | Command | What it does |
@@ -202,7 +182,7 @@ Findings appear as annotations on the PR diff and in the job summary.
202
182
  | `infrawise init` | Detect AWS + repo, generate `infrawise.yaml` |
203
183
  | `infrawise auth` | Select or switch AWS profile |
204
184
  | `infrawise analyze` | Scan repo + AWS, build graph, print findings |
205
- | `infrawise dev` | Start MCP server at `http://localhost:3000/mcp` |
185
+ | `infrawise dev` | Start MCP server auto-analyzes if no cache, watches files for live refresh |
206
186
  | `infrawise doctor` | Validate AWS access, DB connectivity, and config |
207
187
 
208
188
  ---
@@ -425,7 +405,7 @@ src/
425
405
  mongodb.ts, aws.ts, logs.ts, terraform.ts
426
406
  analyzers/ 23 rule-based analyzers
427
407
  context/ Repository scanner (ts-morph AST)
428
- server/ Fastify MCP HTTP server (plain JSON-RPC, no SDK)
408
+ server/ Fastify MCP server (@modelcontextprotocol/sdk, Streamable HTTP)
429
409
  cli/ CLI commands (Commander.js)
430
410
  ```
431
411
 
@@ -512,10 +492,9 @@ pnpm release major # 0.1.2 → 1.0.0 (breaking changes)
512
492
  pnpm release 1.5.0 # explicit version
513
493
 
514
494
  git push origin main v1.2.3
515
- git push origin v1 --force # update the floating major tag
516
495
  ```
517
496
 
518
- Pushing a `v*.*.*` tag creates a draft GitHub release. Publishing that release triggers npm publish automatically.
497
+ `pnpm release` bumps the version, tags, and creates a draft GitHub release with notes generated from commit messages. Push the tag, then publish the draft on GitHub to trigger npm publish.
519
498
 
520
499
  ### PR checklist
521
500
 
@@ -37,7 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.runAnalyze = runAnalyze;
40
- const fs = __importStar(require("fs"));
40
+ exports.runCodeRefresh = runCodeRefresh;
41
41
  const path = __importStar(require("path"));
42
42
  const chalk_1 = __importDefault(require("chalk"));
43
43
  const ora_1 = __importDefault(require("ora"));
@@ -53,72 +53,19 @@ const context_1 = require("../../context");
53
53
  const graph_1 = require("../../graph");
54
54
  const analyzers_1 = require("../../analyzers");
55
55
  const utils_1 = require("../utils");
56
- const SEVERITY_RANK = { high: 3, medium: 2, low: 1, never: 0 };
57
- function mkSpinner(text, ci) {
58
- if (ci) {
59
- return {
60
- succeed: (msg) => console.log(` ✓ ${msg}`),
61
- warn: (msg) => console.log(` ⚠ ${msg}`),
62
- };
63
- }
56
+ function mkSpinner(text) {
64
57
  return (0, ora_1.default)({ text: chalk_1.default.dim(text), color: 'cyan' }).start();
65
58
  }
66
- function emitCIOutput(findings, failOn) {
67
- for (const f of findings) {
68
- const level = f.severity === 'high' ? 'error' : f.severity === 'medium' ? 'warning' : 'notice';
69
- console.log(`::${level} title=Infrawise [${f.severity.toUpperCase()}]::${f.issue} — ${f.description}`);
70
- }
71
- const summaryPath = process.env['GITHUB_STEP_SUMMARY'];
72
- if (summaryPath) {
73
- const high = findings.filter((f) => f.severity === 'high').length;
74
- const medium = findings.filter((f) => f.severity === 'medium').length;
75
- const low = findings.filter((f) => f.severity === 'low').length;
76
- const rows = findings.map((f) => {
77
- const icon = f.severity === 'high' ? '🔴' : f.severity === 'medium' ? '🟡' : '🔵';
78
- return `| ${icon} ${f.severity.toUpperCase()} | ${f.issue} | ${f.recommendation} |`;
79
- }).join('\n');
80
- const summary = findings.length === 0
81
- ? '## ✅ Infrawise — no issues found\n'
82
- : [
83
- '## Infrawise Analysis',
84
- '',
85
- `**${high} high · ${medium} medium · ${low} low**`,
86
- '',
87
- '| Severity | Issue | Recommendation |',
88
- '|---|---|---|',
89
- rows,
90
- ].join('\n');
91
- fs.appendFileSync(summaryPath, summary + '\n');
92
- }
93
- const threshold = SEVERITY_RANK[failOn] ?? 3;
94
- const maxFound = Math.max(0, ...findings.map((f) => SEVERITY_RANK[f.severity] ?? 0));
95
- if (maxFound >= threshold && threshold > 0) {
96
- process.exit(1);
97
- }
98
- }
99
59
  async function runAnalyze(options = {}) {
100
- const ci = options.ci ?? false;
101
- const failOn = options.failOn ?? 'high';
102
- if (!ci)
103
- (0, utils_1.printHeader)('Running Analysis');
60
+ (0, utils_1.printHeader)('Running Analysis');
104
61
  let config;
105
- let isFallback = false;
106
62
  try {
107
63
  config = (0, core_1.loadConfig)(options.config);
108
- if (!ci)
109
- utils_1.log.success('Config loaded', options.config ?? 'infrawise.yaml');
64
+ utils_1.log.success('Config loaded', options.config ?? 'infrawise.yaml');
110
65
  }
111
66
  catch (err) {
112
- const isNotFound = err instanceof Error && err.message.includes('not found at');
113
- if (ci && isNotFound) {
114
- config = { project: path.basename(process.cwd()) };
115
- isFallback = true;
116
- console.log(' ✓ No infrawise.yaml — running code-only analysis (repo scan + IaC)');
117
- }
118
- else {
119
- console.error((0, core_1.formatError)(err));
120
- process.exit(1);
121
- }
67
+ console.error((0, core_1.formatError)(err));
68
+ process.exit(1);
122
69
  }
123
70
  const repoPath = options.repo ?? process.cwd();
124
71
  const awsCfg = { region: config.aws?.region, profile: config.aws?.profile, endpoint: config.aws?.endpoint };
@@ -129,7 +76,7 @@ async function runAnalyze(options = {}) {
129
76
  const servicesMeta = {};
130
77
  // ── DynamoDB ────────────────────────────────────────────────────────────────
131
78
  if (config.dynamodb?.enabled === true) {
132
- const s = mkSpinner('Extracting DynamoDB tables...', ci);
79
+ const s = mkSpinner('Extracting DynamoDB tables...');
133
80
  try {
134
81
  const result = await (0, dynamodb_1.extractDynamoMetadata)(config);
135
82
  dynamoMeta.push(...result);
@@ -141,7 +88,7 @@ async function runAnalyze(options = {}) {
141
88
  }
142
89
  // ── PostgreSQL ──────────────────────────────────────────────────────────────
143
90
  if (config.postgres?.enabled && config.postgres.connectionString) {
144
- const s = mkSpinner('Extracting PostgreSQL schema...', ci);
91
+ const s = mkSpinner('Extracting PostgreSQL schema...');
145
92
  try {
146
93
  const result = await (0, postgres_1.extractPostgresMetadata)(config.postgres.connectionString);
147
94
  postgresMeta.push(...result);
@@ -153,7 +100,7 @@ async function runAnalyze(options = {}) {
153
100
  }
154
101
  // ── MySQL ───────────────────────────────────────────────────────────────────
155
102
  if (config.mysql?.enabled && config.mysql.connectionString) {
156
- const s = mkSpinner('Extracting MySQL schema...', ci);
103
+ const s = mkSpinner('Extracting MySQL schema...');
157
104
  try {
158
105
  const result = await (0, mysql_1.extractMySQLMetadata)(config.mysql.connectionString);
159
106
  mysqlMeta.push(...result);
@@ -165,7 +112,7 @@ async function runAnalyze(options = {}) {
165
112
  }
166
113
  // ── MongoDB ─────────────────────────────────────────────────────────────────
167
114
  if (config.mongodb?.enabled && config.mongodb.connectionString) {
168
- const s = mkSpinner('Extracting MongoDB schema...', ci);
115
+ const s = mkSpinner('Extracting MongoDB schema...');
169
116
  try {
170
117
  const result = await (0, mongodb_1.extractMongoMetadata)(config.mongodb.connectionString, config.mongodb.databases);
171
118
  mongoMeta.push(...result);
@@ -177,7 +124,7 @@ async function runAnalyze(options = {}) {
177
124
  }
178
125
  // ── SQS ─────────────────────────────────────────────────────────────────────
179
126
  if (config.sqs?.enabled === true) {
180
- const s = mkSpinner('Extracting SQS queues...', ci);
127
+ const s = mkSpinner('Extracting SQS queues...');
181
128
  try {
182
129
  const result = await (0, aws_1.extractSQSMetadata)(awsCfg);
183
130
  servicesMeta.sqs = result;
@@ -189,7 +136,7 @@ async function runAnalyze(options = {}) {
189
136
  }
190
137
  // ── SNS ─────────────────────────────────────────────────────────────────────
191
138
  if (config.sns?.enabled === true) {
192
- const s = mkSpinner('Extracting SNS topics...', ci);
139
+ const s = mkSpinner('Extracting SNS topics...');
193
140
  try {
194
141
  const result = await (0, aws_1.extractSNSMetadata)(awsCfg);
195
142
  servicesMeta.sns = result;
@@ -201,7 +148,7 @@ async function runAnalyze(options = {}) {
201
148
  }
202
149
  // ── SSM Parameter Store ──────────────────────────────────────────────────────
203
150
  if (config.ssm?.enabled === true) {
204
- const s = mkSpinner('Extracting SSM parameters...', ci);
151
+ const s = mkSpinner('Extracting SSM parameters...');
205
152
  try {
206
153
  const result = await (0, aws_1.extractSSMMetadata)({ ...awsCfg, paths: config.ssm?.paths });
207
154
  servicesMeta.ssm = result;
@@ -213,7 +160,7 @@ async function runAnalyze(options = {}) {
213
160
  }
214
161
  // ── Secrets Manager ──────────────────────────────────────────────────────────
215
162
  if (config.secretsManager?.enabled === true) {
216
- const s = mkSpinner('Extracting Secrets Manager metadata...', ci);
163
+ const s = mkSpinner('Extracting Secrets Manager metadata...');
217
164
  try {
218
165
  const result = await (0, aws_1.extractSecretsMetadata)(awsCfg);
219
166
  servicesMeta.secrets = result;
@@ -225,7 +172,7 @@ async function runAnalyze(options = {}) {
225
172
  }
226
173
  // ── Lambda ───────────────────────────────────────────────────────────────────
227
174
  if (config.lambda?.enabled === true) {
228
- const s = mkSpinner('Extracting Lambda functions...', ci);
175
+ const s = mkSpinner('Extracting Lambda functions...');
229
176
  try {
230
177
  const result = await (0, aws_1.extractLambdaMetadata)(awsCfg);
231
178
  servicesMeta.lambda = result;
@@ -237,7 +184,7 @@ async function runAnalyze(options = {}) {
237
184
  }
238
185
  // ── RDS ──────────────────────────────────────────────────────────────────────
239
186
  if (config.rds?.enabled === true) {
240
- const s = mkSpinner('Extracting RDS instances...', ci);
187
+ const s = mkSpinner('Extracting RDS instances...');
241
188
  try {
242
189
  const result = await (0, aws_1.extractRDSMetadata)(awsCfg);
243
190
  servicesMeta.rds = result;
@@ -249,7 +196,7 @@ async function runAnalyze(options = {}) {
249
196
  }
250
197
  // ── CloudWatch Logs ──────────────────────────────────────────────────────────
251
198
  if (config.cloudwatchLogs?.enabled) {
252
- const s = mkSpinner('Sampling CloudWatch Logs (errors only, max 50 groups)...', ci);
199
+ const s = mkSpinner('Sampling CloudWatch Logs (errors only, max 50 groups)...');
253
200
  try {
254
201
  const result = await (0, logs_1.extractLogsMetadata)({
255
202
  ...awsCfg,
@@ -267,7 +214,7 @@ async function runAnalyze(options = {}) {
267
214
  // ── IaC schema (Terraform / CloudFormation / CDK) ────────────────────────────
268
215
  let iacDriftAnalyzer;
269
216
  {
270
- const s = mkSpinner('Extracting IaC schema (Terraform / CloudFormation / CDK)...', ci);
217
+ const s = mkSpinner('Extracting IaC schema (Terraform / CloudFormation / CDK)...');
271
218
  try {
272
219
  const iacSchema = await (0, terraform_1.extractIaCSchema)(repoPath);
273
220
  const total = iacSchema.dynamoTables.length + iacSchema.rdsInstances.length +
@@ -285,7 +232,7 @@ async function runAnalyze(options = {}) {
285
232
  // ── Repository scan ──────────────────────────────────────────────────────────
286
233
  let operations;
287
234
  {
288
- const s = mkSpinner(`Scanning ${path.basename(repoPath)} for service usage...`, ci);
235
+ const s = mkSpinner(`Scanning ${path.basename(repoPath)} for service usage...`);
289
236
  try {
290
237
  operations = await (0, context_1.scanRepository)(repoPath);
291
238
  s.succeed(chalk_1.default.green('Repository scanned') + chalk_1.default.dim(` ${operations.length} service operation(s) found`));
@@ -298,30 +245,30 @@ async function runAnalyze(options = {}) {
298
245
  // ── Build graph ──────────────────────────────────────────────────────────────
299
246
  let graph;
300
247
  {
301
- const s = mkSpinner('Building infrastructure graph...', ci);
248
+ const s = mkSpinner('Building infrastructure graph...');
302
249
  graph = (0, graph_1.buildGraph)(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
303
250
  s.succeed(chalk_1.default.green('Graph built') + chalk_1.default.dim(` ${graph.nodes.length} nodes, ${graph.edges.length} edges`));
304
251
  }
305
252
  // ── Run analyzers ────────────────────────────────────────────────────────────
306
253
  let findings;
307
254
  {
308
- const s = mkSpinner('Running analyzers...', ci);
255
+ const s = mkSpinner('Running analyzers...');
309
256
  const analyzers = [
310
- ...(config.dynamodb?.enabled === true || isFallback ? [
257
+ ...(config.dynamodb?.enabled === true ? [
311
258
  new analyzers_1.FullTableScanAnalyzer(),
312
259
  new analyzers_1.MissingGSIAnalyzer(),
313
260
  new analyzers_1.HotPartitionAnalyzer(),
314
261
  ] : []),
315
- ...(config.postgres?.enabled || isFallback ? [
262
+ ...(config.postgres?.enabled ? [
316
263
  new analyzers_1.MissingIndexAnalyzer(),
317
264
  new analyzers_1.NplusOneAnalyzer(),
318
265
  new analyzers_1.LargeSelectAnalyzer(),
319
266
  ] : []),
320
- ...(config.mysql?.enabled || isFallback ? [
267
+ ...(config.mysql?.enabled ? [
321
268
  new analyzers_1.MissingMySQLIndexAnalyzer(),
322
269
  new analyzers_1.MySQLFullTableScanAnalyzer(),
323
270
  ] : []),
324
- ...(config.mongodb?.enabled || isFallback ? [
271
+ ...(config.mongodb?.enabled ? [
325
272
  new analyzers_1.MissingMongoIndexAnalyzer(),
326
273
  new analyzers_1.MongoCollectionScanAnalyzer(),
327
274
  ] : []),
@@ -356,11 +303,8 @@ async function runAnalyze(options = {}) {
356
303
  (0, core_1.writeCache)('graph', graph);
357
304
  (0, core_1.writeCache)('findings', findings);
358
305
  (0, core_1.writeCache)('operations', operations);
306
+ (0, core_1.writeCache)('meta', { dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta });
359
307
  // ── Output ────────────────────────────────────────────────────────────────────
360
- if (ci) {
361
- emitCIOutput(findings, failOn);
362
- return;
363
- }
364
308
  console.log('');
365
309
  if (findings.length === 0) {
366
310
  console.log(` ${chalk_1.default.green.bold('✓ No issues found!')} ${chalk_1.default.dim('Your infrastructure looks clean.')}`);
@@ -378,3 +322,62 @@ async function runAnalyze(options = {}) {
378
322
  utils_1.log.info(`Run ${chalk_1.default.cyan('infrawise dev')} to explore via the MCP server`);
379
323
  console.log('');
380
324
  }
325
+ async function runCodeRefresh(repoPath, config) {
326
+ const cached = (0, core_1.readCache)('meta', Infinity);
327
+ const dynamoMeta = cached?.dynamoMeta ?? [];
328
+ const postgresMeta = cached?.postgresMeta ?? [];
329
+ const mysqlMeta = cached?.mysqlMeta ?? [];
330
+ const mongoMeta = cached?.mongoMeta ?? [];
331
+ const servicesMeta = cached?.servicesMeta ?? {};
332
+ // Re-run IaC schema (pure file scan, no AWS calls)
333
+ let iacDriftAnalyzer;
334
+ try {
335
+ const iacSchema = await (0, terraform_1.extractIaCSchema)(repoPath);
336
+ iacDriftAnalyzer = new analyzers_1.IaCDriftAnalyzer();
337
+ iacDriftAnalyzer.setIaCSchema(iacSchema);
338
+ }
339
+ catch {
340
+ // IaC scan is best-effort
341
+ }
342
+ // Re-run repo scan
343
+ let operations;
344
+ try {
345
+ operations = await (0, context_1.scanRepository)(repoPath);
346
+ }
347
+ catch {
348
+ operations = [];
349
+ }
350
+ const graph = (0, graph_1.buildGraph)(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
351
+ const analyzers = [
352
+ ...(config.dynamodb?.enabled === true ? [
353
+ new analyzers_1.FullTableScanAnalyzer(), new analyzers_1.MissingGSIAnalyzer(), new analyzers_1.HotPartitionAnalyzer(),
354
+ ] : []),
355
+ ...(config.postgres?.enabled ? [
356
+ new analyzers_1.MissingIndexAnalyzer(), new analyzers_1.NplusOneAnalyzer(), new analyzers_1.LargeSelectAnalyzer(),
357
+ ] : []),
358
+ ...(config.mysql?.enabled ? [
359
+ new analyzers_1.MissingMySQLIndexAnalyzer(), new analyzers_1.MySQLFullTableScanAnalyzer(),
360
+ ] : []),
361
+ ...(config.mongodb?.enabled ? [
362
+ new analyzers_1.MissingMongoIndexAnalyzer(), new analyzers_1.MongoCollectionScanAnalyzer(),
363
+ ] : []),
364
+ ...(config.sqs?.enabled === true ? [
365
+ new analyzers_1.MissingDLQAnalyzer(), new analyzers_1.UnencryptedQueueAnalyzer(), new analyzers_1.LargeQueueBacklogAnalyzer(),
366
+ ] : []),
367
+ ...(config.secretsManager?.enabled === true ? [new analyzers_1.MissingSecretRotationAnalyzer()] : []),
368
+ ...(config.cloudwatchLogs?.enabled ? [new analyzers_1.MissingLogRetentionAnalyzer()] : []),
369
+ ...(config.lambda?.enabled === true ? [
370
+ new analyzers_1.LambdaDefaultMemoryAnalyzer(), new analyzers_1.LambdaHighTimeoutAnalyzer(),
371
+ ] : []),
372
+ ...(config.rds?.enabled === true ? [
373
+ new analyzers_1.RDSPubliclyAccessibleAnalyzer(), new analyzers_1.RDSNoBackupAnalyzer(), new analyzers_1.RDSUnencryptedAnalyzer(),
374
+ new analyzers_1.RDSNoDeletionProtectionAnalyzer(), new analyzers_1.RDSNoMultiAZAnalyzer(),
375
+ ] : []),
376
+ ...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
377
+ ];
378
+ const findings = await (0, analyzers_1.runAllAnalyzers)(graph, analyzers);
379
+ (0, core_1.writeCache)('graph', graph);
380
+ (0, core_1.writeCache)('findings', findings);
381
+ (0, core_1.writeCache)('operations', operations);
382
+ return { graph, findings };
383
+ }
@@ -1,14 +1,50 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.runDev = runDev;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
7
42
  const chalk_1 = __importDefault(require("chalk"));
8
43
  const ora_1 = __importDefault(require("ora"));
9
44
  const core_1 = require("../../core");
10
45
  const server_1 = require("../../server");
11
46
  const utils_1 = require("../utils");
47
+ const analyze_1 = require("./analyze");
12
48
  const BOX_W = 52;
13
49
  const TOOL_MAP = [
14
50
  { name: 'get_infra_overview' },
@@ -67,7 +103,8 @@ async function runDev(options = {}) {
67
103
  console.error((0, core_1.formatError)(err));
68
104
  process.exit(1);
69
105
  }
70
- // Load cached state
106
+ const repoPath = process.cwd();
107
+ // Auto-analyze if no cache
71
108
  const cachedGraph = (0, core_1.readCache)('graph');
72
109
  const cachedFindings = (0, core_1.readCache)('findings');
73
110
  if (cachedGraph && cachedFindings) {
@@ -75,9 +112,12 @@ async function runDev(options = {}) {
75
112
  (0, server_1.setGraphState)(cachedGraph, cachedFindings);
76
113
  }
77
114
  else {
78
- utils_1.log.warn('No cached analysis found');
79
- utils_1.log.dim(`Run ${chalk_1.default.cyan('infrawise analyze')} first for full results`);
80
- (0, server_1.setGraphState)({ nodes: [], edges: [] }, []);
115
+ utils_1.log.warn('No cache found — running analysis now...');
116
+ console.log('');
117
+ await (0, analyze_1.runAnalyze)({ repo: repoPath, config: options.config });
118
+ const freshGraph = (0, core_1.readCache)('graph') ?? { nodes: [], edges: [] };
119
+ const freshFindings = (0, core_1.readCache)('findings') ?? [];
120
+ (0, server_1.setGraphState)(freshGraph, freshFindings);
81
121
  }
82
122
  console.log('');
83
123
  // Start server
@@ -90,7 +130,6 @@ async function runDev(options = {}) {
90
130
  const inactiveTools = TOOL_MAP.filter((t) => !isEnabled(config, t.service)).map((t) => t.name);
91
131
  // URL rows
92
132
  const mcpUrl = `http://localhost:${port}/mcp`;
93
- const toolsUrl = `http://localhost:${port}/mcp/tools`;
94
133
  const healthUrl = `http://localhost:${port}/health`;
95
134
  // Print box
96
135
  console.log('');
@@ -98,7 +137,6 @@ async function runDev(options = {}) {
98
137
  boxLine(' MCP Server', chalk_1.default.bold(' MCP Server'));
99
138
  boxDivider();
100
139
  boxLine(` POST ${mcpUrl}`, ` ${chalk_1.default.dim('POST')} ${chalk_1.default.cyan(mcpUrl)}`);
101
- boxLine(` GET ${toolsUrl}`, ` ${chalk_1.default.dim('GET')} ${chalk_1.default.cyan(toolsUrl)}`);
102
140
  boxLine(` GET ${healthUrl}`, ` ${chalk_1.default.dim('GET')} ${chalk_1.default.cyan(healthUrl)}`);
103
141
  boxDivider();
104
142
  const activeLabel = ` Tools (${activeTools.length} active${inactiveTools.length > 0 ? ` · ${inactiveTools.length} off` : ''})`;
@@ -118,7 +156,49 @@ async function runDev(options = {}) {
118
156
  console.log(chalk_1.default.dim(' Add via CLI:'));
119
157
  console.log(chalk_1.default.dim(` claude mcp add --transport http infrawise ${mcpUrl}`));
120
158
  console.log('');
121
- console.log(chalk_1.default.dim(' Press Ctrl+C to stop\n'));
159
+ console.log(chalk_1.default.dim(' Watching for file changes... Press Ctrl+C to stop\n'));
160
+ // File watch — re-run code analysis on save, skip slow AWS/DB extraction
161
+ let debounceTimer = null;
162
+ let refreshing = false;
163
+ const configFile = path.resolve(options.config ?? 'infrawise.yaml');
164
+ try {
165
+ fs.watch(repoPath, { recursive: true }, (_, filename) => {
166
+ if (!filename)
167
+ return;
168
+ const abs = path.join(repoPath, filename);
169
+ if (abs === configFile) {
170
+ console.log(chalk_1.default.dim('\n infrawise.yaml changed — restart to apply config changes\n'));
171
+ return;
172
+ }
173
+ const ext = path.extname(filename);
174
+ if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
175
+ return;
176
+ if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
177
+ return;
178
+ if (debounceTimer)
179
+ clearTimeout(debounceTimer);
180
+ debounceTimer = setTimeout(async () => {
181
+ if (refreshing)
182
+ return;
183
+ refreshing = true;
184
+ const spin = (0, ora_1.default)({ text: chalk_1.default.dim('Refreshing code analysis...'), color: 'cyan' }).start();
185
+ try {
186
+ const { graph, findings } = await (0, analyze_1.runCodeRefresh)(repoPath, config);
187
+ (0, server_1.setGraphState)(graph, findings);
188
+ spin.succeed(chalk_1.default.green('Analysis refreshed') + chalk_1.default.dim(` ${graph.nodes.length} nodes · ${findings.length} finding(s)`));
189
+ }
190
+ catch (err) {
191
+ spin.warn(chalk_1.default.yellow('Refresh failed') + chalk_1.default.dim(` ${err instanceof Error ? err.message : String(err)}`));
192
+ }
193
+ finally {
194
+ refreshing = false;
195
+ }
196
+ }, 2000);
197
+ });
198
+ }
199
+ catch {
200
+ // fs.watch may not support recursive on all platforms — silently skip
201
+ }
122
202
  process.on('SIGINT', () => {
123
203
  console.log(chalk_1.default.dim('\n Shutting down...\n'));
124
204
  process.exit(0);
package/dist/cli/index.js CHANGED
@@ -37,17 +37,12 @@ program
37
37
  .option('-c, --config <path>', 'Path to infrawise.yaml', 'infrawise.yaml')
38
38
  .option('-r, --repo <path>', 'Path to repository to scan', process.cwd())
39
39
  .option('--no-cache', 'Skip reading/writing the cache')
40
- .option('--ci', 'CI mode: GitHub annotations output, job summary, exit code on findings')
41
- .option('--fail-on <severity>', 'Exit 1 if findings at or above this severity (high|medium|low|never)', 'high')
42
40
  .action(async (options) => {
43
- if (!options.ci)
44
- (0, utils_1.printBanner)();
41
+ (0, utils_1.printBanner)();
45
42
  await (0, analyze_1.runAnalyze)({
46
43
  config: options.config !== 'infrawise.yaml' ? options.config : undefined,
47
44
  repo: options.repo,
48
45
  noCache: !options.cache,
49
- ci: options.ci ?? false,
50
- failOn: options.failOn ?? 'high',
51
46
  });
52
47
  });
53
48
  program
@@ -74,6 +74,7 @@ exports.InfrawiseConfigSchema = zod_1.z.object({
74
74
  secretsManager: zod_1.z.object({ enabled: zod_1.z.boolean().optional().default(true) }).optional(),
75
75
  lambda: zod_1.z.object({ enabled: zod_1.z.boolean().optional().default(true) }).optional(),
76
76
  rds: zod_1.z.object({ enabled: zod_1.z.boolean().optional().default(false) }).optional(),
77
+ kafka: zod_1.z.object({ enabled: zod_1.z.boolean().optional().default(false) }).optional(),
77
78
  cloudwatchLogs: zod_1.z.object({
78
79
  enabled: zod_1.z.boolean().optional().default(false),
79
80
  logGroupPrefixes: zod_1.z.array(zod_1.z.string()).optional(),
@@ -5,10 +5,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.currentFindings = exports.currentGraph = void 0;
7
7
  exports.setGraphState = setGraphState;
8
+ exports.createMcpServer = createMcpServer;
8
9
  exports.createServer = createServer;
10
+ const fs_1 = require("fs");
11
+ const path_1 = require("path");
9
12
  const fastify_1 = __importDefault(require("fastify"));
10
13
  const cors_1 = __importDefault(require("@fastify/cors"));
14
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
15
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
16
+ const zod_1 = require("zod");
11
17
  const core_1 = require("../core");
18
+ const { version } = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../package.json'), 'utf8'));
12
19
  const analyzers_1 = require("../analyzers");
13
20
  const graph_1 = require("../graph");
14
21
  // ── State ────────────────────────────────────────────────────────────────────
@@ -20,317 +27,249 @@ function setGraphState(graph, findings) {
20
27
  exports.currentGraph = currentGraph = graph;
21
28
  exports.currentFindings = currentFindings = findings;
22
29
  }
30
+ // ── Helpers ──────────────────────────────────────────────────────────────────
23
31
  function toText(data) {
24
32
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
25
33
  }
26
- // ── Tools ────────────────────────────────────────────────────────────────────
27
- const TOOLS = [
28
- {
29
- name: 'get_infra_overview',
34
+ function logged(name, fn) {
35
+ return async (args) => {
36
+ const hasArgs = Object.keys(args).length > 0;
37
+ core_1.logger.info(`→ ${name}${hasArgs ? ` ${JSON.stringify(args)}` : ''}`);
38
+ return fn(args);
39
+ };
40
+ }
41
+ // ── MCP Server ────────────────────────────────────────────────────────────────
42
+ function createMcpServer() {
43
+ const mcp = new mcp_js_1.McpServer({ name: 'infrawise', version });
44
+ mcp.registerTool('get_infra_overview', {
30
45
  description: 'Returns a complete snapshot of all infrastructure: databases, queues, topics, secrets, parameters, log groups, lambdas, and all findings. Start here for a full picture.',
31
- inputSchema: { type: 'object', properties: {} },
32
- handler: async () => {
33
- const tables = (0, graph_1.getTableNodes)(currentGraph);
34
- const queues = (0, graph_1.getQueueNodes)(currentGraph);
35
- const topics = (0, graph_1.getTopicNodes)(currentGraph);
36
- const secrets = (0, graph_1.getSecretNodes)(currentGraph);
37
- const parameters = (0, graph_1.getParameterNodes)(currentGraph);
38
- const logGroups = (0, graph_1.getLogGroupNodes)(currentGraph);
39
- const lambdas = (0, graph_1.getLambdaNodes)(currentGraph);
40
- const functions = (0, graph_1.getFunctionNodes)(currentGraph);
41
- return toText({
42
- summary: {
43
- tables: tables.length, functions: functions.length,
44
- queues: queues.length, topics: topics.length,
45
- secrets: secrets.length, parameters: parameters.length,
46
- logGroups: logGroups.length, lambdas: lambdas.length,
47
- totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
48
- findings: (0, analyzers_1.summarizeFindings)(currentFindings),
49
- },
50
- databases: tables.map((t) => ({ name: t.name, type: t.databaseType })),
51
- queues: queues.map((q) => ({ name: q.name, hasDLQ: q.hasDLQ, encrypted: q.encrypted, approximateMessages: q.approximateMessages })),
52
- topics: topics.map((t) => ({ name: t.name, subscriptions: t.subscriptionCount })),
53
- secrets: secrets.map((s) => ({ name: s.name, rotationEnabled: s.rotationEnabled })),
54
- parameters: parameters.map((p) => ({ name: p.name, type: p.paramType, tier: p.tier })),
55
- lambdas: lambdas.map((l) => ({ name: l.name, runtime: l.runtime, memoryMB: l.memoryMB })),
56
- logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never', errorCount: lg.errorCount })),
57
- highFindings: currentFindings.filter((f) => f.severity === 'high').map((f) => ({ issue: f.issue, recommendation: f.recommendation })),
58
- });
59
- },
60
- },
61
- {
62
- name: 'get_graph_summary',
63
- description: 'Returns the full infrastructure graph (all nodes and edges) plus findings summary.',
64
- inputSchema: { type: 'object', properties: {} },
65
- handler: async () => toText({
66
- nodes: currentGraph.nodes,
67
- edges: currentGraph.edges,
68
- findings: currentFindings,
46
+ inputSchema: zod_1.z.object({}),
47
+ }, logged('get_infra_overview', async () => {
48
+ const tables = (0, graph_1.getTableNodes)(currentGraph);
49
+ const queues = (0, graph_1.getQueueNodes)(currentGraph);
50
+ const topics = (0, graph_1.getTopicNodes)(currentGraph);
51
+ const secrets = (0, graph_1.getSecretNodes)(currentGraph);
52
+ const parameters = (0, graph_1.getParameterNodes)(currentGraph);
53
+ const logGroups = (0, graph_1.getLogGroupNodes)(currentGraph);
54
+ const lambdas = (0, graph_1.getLambdaNodes)(currentGraph);
55
+ const functions = (0, graph_1.getFunctionNodes)(currentGraph);
56
+ return toText({
69
57
  summary: {
58
+ tables: tables.length, functions: functions.length,
59
+ queues: queues.length, topics: topics.length,
60
+ secrets: secrets.length, parameters: parameters.length,
61
+ logGroups: logGroups.length, lambdas: lambdas.length,
70
62
  totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
71
- tables: (0, graph_1.getTableNodes)(currentGraph).length, functions: (0, graph_1.getFunctionNodes)(currentGraph).length,
72
- queues: (0, graph_1.getQueueNodes)(currentGraph).length, scans: (0, graph_1.getScanEdges)(currentGraph).length,
73
- ...(0, analyzers_1.summarizeFindings)(currentFindings),
63
+ findings: (0, analyzers_1.summarizeFindings)(currentFindings),
74
64
  },
75
- }),
76
- },
77
- {
78
- name: 'analyze_function',
79
- description: 'Analyze a specific function for all infrastructure issues: DB queries, queue publishing, secret access, etc.',
80
- inputSchema: {
81
- type: 'object',
82
- properties: { function: { type: 'string', description: 'Function name to analyze' } },
83
- required: ['function'],
84
- },
85
- handler: async ({ function: functionName }) => {
86
- const funcNode = currentGraph.nodes.find((n) => n.type === 'function' && n.name === functionName);
87
- if (!funcNode) {
88
- return toText({ function: functionName, found: false, issues: [], recommendations: [`Function "${String(functionName)}" not found in the analyzed codebase.`] });
89
- }
90
- const outEdges = (0, graph_1.getOutgoingEdges)(currentGraph, funcNode.id);
91
- const relatedFindings = currentFindings.filter((f) => {
92
- const meta = f.metadata;
93
- return meta?.functionName === functionName || String(meta?.callerFunctions ?? '').includes(String(functionName));
94
- });
95
- return toText({
96
- function: functionName, found: true,
97
- file: funcNode.type === 'function' ? funcNode.file : undefined,
98
- accesses: outEdges.map((e) => {
99
- const target = currentGraph.nodes.find((n) => n.id === e.to);
100
- return { targetId: e.to, edgeType: e.type, targetName: target && 'name' in target ? target.name : e.to, targetType: target?.type };
101
- }),
102
- issues: relatedFindings.map((f) => ({ severity: f.severity, issue: f.issue, description: f.description })),
103
- recommendations: [...new Set(relatedFindings.map((f) => f.recommendation))],
104
- });
65
+ databases: tables.map((t) => ({ name: t.name, type: t.databaseType })),
66
+ queues: queues.map((q) => ({ name: q.name, hasDLQ: q.hasDLQ, encrypted: q.encrypted, approximateMessages: q.approximateMessages })),
67
+ topics: topics.map((t) => ({ name: t.name, subscriptions: t.subscriptionCount })),
68
+ secrets: secrets.map((s) => ({ name: s.name, rotationEnabled: s.rotationEnabled })),
69
+ parameters: parameters.map((p) => ({ name: p.name, type: p.paramType, tier: p.tier })),
70
+ lambdas: lambdas.map((l) => ({ name: l.name, runtime: l.runtime, memoryMB: l.memoryMB })),
71
+ logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never', errorCount: lg.errorCount })),
72
+ highFindings: currentFindings.filter((f) => f.severity === 'high').map((f) => ({ issue: f.issue, recommendation: f.recommendation })),
73
+ });
74
+ }));
75
+ mcp.registerTool('get_graph_summary', {
76
+ description: 'Returns the full infrastructure graph (all nodes and edges) plus findings summary.',
77
+ inputSchema: zod_1.z.object({}),
78
+ }, logged('get_graph_summary', async () => toText({
79
+ nodes: currentGraph.nodes,
80
+ edges: currentGraph.edges,
81
+ findings: currentFindings,
82
+ summary: {
83
+ totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
84
+ tables: (0, graph_1.getTableNodes)(currentGraph).length, functions: (0, graph_1.getFunctionNodes)(currentGraph).length,
85
+ queues: (0, graph_1.getQueueNodes)(currentGraph).length, scans: (0, graph_1.getScanEdges)(currentGraph).length,
86
+ ...(0, analyzers_1.summarizeFindings)(currentFindings),
105
87
  },
106
- },
107
- {
108
- name: 'suggest_gsi',
88
+ })));
89
+ mcp.registerTool('analyze_function', {
90
+ description: 'Analyze a specific function for all infrastructure issues: DB queries, queue publishing, secret access, etc.',
91
+ inputSchema: zod_1.z.object({ function: zod_1.z.string().describe('Function name to analyze') }),
92
+ }, logged('analyze_function', async ({ function: functionName }) => {
93
+ const funcNode = currentGraph.nodes.find((n) => n.type === 'function' && n.name === functionName);
94
+ if (!funcNode) {
95
+ return toText({ function: functionName, found: false, issues: [], recommendations: [`Function "${functionName}" not found in the analyzed codebase.`] });
96
+ }
97
+ const outEdges = (0, graph_1.getOutgoingEdges)(currentGraph, funcNode.id);
98
+ const relatedFindings = currentFindings.filter((f) => {
99
+ const meta = f.metadata;
100
+ return meta?.functionName === functionName || String(meta?.callerFunctions ?? '').includes(functionName);
101
+ });
102
+ return toText({
103
+ function: functionName, found: true,
104
+ file: funcNode.type === 'function' ? funcNode.file : undefined,
105
+ accesses: outEdges.map((e) => {
106
+ const target = currentGraph.nodes.find((n) => n.id === e.to);
107
+ return { targetId: e.to, edgeType: e.type, targetName: target && 'name' in target ? target.name : e.to, targetType: target?.type };
108
+ }),
109
+ issues: relatedFindings.map((f) => ({ severity: f.severity, issue: f.issue, description: f.description })),
110
+ recommendations: [...new Set(relatedFindings.map((f) => f.recommendation))],
111
+ });
112
+ }));
113
+ mcp.registerTool('suggest_gsi', {
109
114
  description: 'Get GSI suggestions for a DynamoDB table and attribute',
110
- inputSchema: {
111
- type: 'object',
112
- properties: {
113
- table: { type: 'string', description: 'DynamoDB table name' },
114
- attribute: { type: 'string', description: 'Attribute to create the GSI on' },
115
- },
116
- required: ['table', 'attribute'],
117
- },
118
- handler: async ({ table: tableName, attribute }) => {
119
- const sanitizedAttr = String(attribute).replace(/[^a-zA-Z0-9_]/g, '_');
120
- const indexName = `${String(tableName)}-${sanitizedAttr}-index`;
121
- const tableNode = currentGraph.nodes.find((n) => n.type === 'table' && n.databaseType === 'dynamodb' && 'name' in n && n.name === tableName);
122
- return toText({
123
- table: tableName, attribute, found: !!tableNode,
124
- index: { name: indexName, partitionKey: attribute, projectionType: 'ALL', billingMode: 'PAY_PER_REQUEST' },
125
- rationale: `A GSI on "${String(attribute)}" allows Query instead of Scan when filtering by this attribute.`,
126
- recommendation: `Add GSI "${indexName}" with partition key "${String(attribute)}" to your IaC definition.`,
127
- });
128
- },
129
- },
130
- {
131
- name: 'postgres_index_suggestions',
115
+ inputSchema: zod_1.z.object({
116
+ table: zod_1.z.string().describe('DynamoDB table name'),
117
+ attribute: zod_1.z.string().describe('Attribute to create the GSI on'),
118
+ }),
119
+ }, logged('suggest_gsi', async ({ table: tableName, attribute }) => {
120
+ const sanitizedAttr = attribute.replace(/[^a-zA-Z0-9_]/g, '_');
121
+ const indexName = `${tableName}-${sanitizedAttr}-index`;
122
+ const tableNode = currentGraph.nodes.find((n) => n.type === 'table' && n.databaseType === 'dynamodb' && 'name' in n && n.name === tableName);
123
+ return toText({
124
+ table: tableName, attribute, found: !!tableNode,
125
+ index: { name: indexName, partitionKey: attribute, projectionType: 'ALL', billingMode: 'PAY_PER_REQUEST' },
126
+ rationale: `A GSI on "${attribute}" allows Query instead of Scan when filtering by this attribute.`,
127
+ recommendation: `Add GSI "${indexName}" with partition key "${attribute}" to your IaC definition.`,
128
+ });
129
+ }));
130
+ mcp.registerTool('postgres_index_suggestions', {
132
131
  description: 'Get PostgreSQL index suggestions for a table column',
133
- inputSchema: {
134
- type: 'object',
135
- properties: {
136
- table: { type: 'string', description: 'PostgreSQL table name' },
137
- column: { type: 'string', description: 'Column name to index' },
138
- },
139
- required: ['table', 'column'],
140
- },
141
- handler: async ({ table: tableName, column }) => {
142
- const sanitizedCol = String(column).replace(/[^a-zA-Z0-9_]/g, '_');
143
- const sanitizedTable = String(tableName).replace(/[^a-zA-Z0-9_]/g, '_');
144
- const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
145
- return toText({
146
- table: tableName, column,
147
- recommendation: `CREATE INDEX CONCURRENTLY ${indexName} ON ${String(tableName)} (${String(column)});`,
148
- rationale: `An index on "${String(column)}" eliminates sequential scans when filtering on this column.`,
149
- notes: ['Use CONCURRENTLY to avoid locking the table', 'Run ANALYZE after creation',
150
- `Partial index: CREATE INDEX CONCURRENTLY ${indexName}_partial ON ${String(tableName)} (${String(column)}) WHERE ${String(column)} IS NOT NULL;`],
151
- });
152
- },
153
- },
154
- {
155
- name: 'suggest_mongo_index',
132
+ inputSchema: zod_1.z.object({
133
+ table: zod_1.z.string().describe('PostgreSQL table name'),
134
+ column: zod_1.z.string().describe('Column name to index'),
135
+ }),
136
+ }, logged('postgres_index_suggestions', async ({ table: tableName, column }) => {
137
+ const sanitizedCol = column.replace(/[^a-zA-Z0-9_]/g, '_');
138
+ const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_]/g, '_');
139
+ const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
140
+ return toText({
141
+ table: tableName, column,
142
+ recommendation: `CREATE INDEX CONCURRENTLY ${indexName} ON ${tableName} (${column});`,
143
+ rationale: `An index on "${column}" eliminates sequential scans when filtering on this column.`,
144
+ notes: ['Use CONCURRENTLY to avoid locking the table', 'Run ANALYZE after creation',
145
+ `Partial index: CREATE INDEX CONCURRENTLY ${indexName}_partial ON ${tableName} (${column}) WHERE ${column} IS NOT NULL;`],
146
+ });
147
+ }));
148
+ mcp.registerTool('suggest_mongo_index', {
156
149
  description: 'Get index suggestions for a MongoDB collection field',
157
- inputSchema: {
158
- type: 'object',
159
- properties: {
160
- collection: { type: 'string', description: 'MongoDB collection name' },
161
- field: { type: 'string', description: 'Field name to index' },
162
- },
163
- required: ['collection', 'field'],
164
- },
165
- handler: async ({ collection, field }) => toText({
166
- collection, field,
167
- recommendation: `db.${String(collection)}.createIndex({ ${String(field)}: 1 })`,
168
- rationale: `An index on "${String(field)}" eliminates full collection scans when filtering on this field.`,
169
- notes: [
170
- `Compound: db.${String(collection)}.createIndex({ ${String(field)}: 1, otherField: 1 })`,
171
- `Text: db.${String(collection)}.createIndex({ ${String(field)}: "text" })`,
172
- `Verify: db.${String(collection)}.explain("executionStats").find({ ${String(field)}: value })`,
173
- ],
150
+ inputSchema: zod_1.z.object({
151
+ collection: zod_1.z.string().describe('MongoDB collection name'),
152
+ field: zod_1.z.string().describe('Field name to index'),
174
153
  }),
175
- },
176
- {
177
- name: 'mysql_index_suggestions',
154
+ }, logged('suggest_mongo_index', async ({ collection, field }) => toText({
155
+ collection, field,
156
+ recommendation: `db.${collection}.createIndex({ ${field}: 1 })`,
157
+ rationale: `An index on "${field}" eliminates full collection scans when filtering on this field.`,
158
+ notes: [
159
+ `Compound: db.${collection}.createIndex({ ${field}: 1, otherField: 1 })`,
160
+ `Text: db.${collection}.createIndex({ ${field}: "text" })`,
161
+ `Verify: db.${collection}.explain("executionStats").find({ ${field}: value })`,
162
+ ],
163
+ })));
164
+ mcp.registerTool('mysql_index_suggestions', {
178
165
  description: 'Get MySQL index suggestions for a table column',
179
- inputSchema: {
180
- type: 'object',
181
- properties: {
182
- table: { type: 'string', description: 'MySQL table name' },
183
- column: { type: 'string', description: 'Column name to index' },
184
- },
185
- required: ['table', 'column'],
186
- },
187
- handler: async ({ table: tableName, column }) => {
188
- const sanitizedCol = String(column).replace(/[^a-zA-Z0-9_]/g, '_');
189
- const sanitizedTable = String(tableName).replace(/[^a-zA-Z0-9_]/g, '_');
190
- const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
191
- return toText({
192
- table: tableName, column,
193
- recommendation: `ALTER TABLE ${String(tableName)} ADD INDEX ${indexName} (${String(column)});`,
194
- rationale: `An index on "${String(column)}" eliminates full table scans when filtering on this column.`,
195
- notes: ['MySQL InnoDB adds indexes online (no full lock for 5.6+)', 'EXPLAIN SELECT ... to verify after adding',
196
- `Composite: ALTER TABLE ${String(tableName)} ADD INDEX idx_composite (${String(column)}, other_column);`],
197
- });
198
- },
199
- },
200
- {
201
- name: 'get_queue_details',
166
+ inputSchema: zod_1.z.object({
167
+ table: zod_1.z.string().describe('MySQL table name'),
168
+ column: zod_1.z.string().describe('Column name to index'),
169
+ }),
170
+ }, logged('mysql_index_suggestions', async ({ table: tableName, column }) => {
171
+ const sanitizedCol = column.replace(/[^a-zA-Z0-9_]/g, '_');
172
+ const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_]/g, '_');
173
+ const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
174
+ return toText({
175
+ table: tableName, column,
176
+ recommendation: `ALTER TABLE ${tableName} ADD INDEX ${indexName} (${column});`,
177
+ rationale: `An index on "${column}" eliminates full table scans when filtering on this column.`,
178
+ notes: ['MySQL InnoDB adds indexes online (no full lock for 5.6+)', 'EXPLAIN SELECT ... to verify after adding',
179
+ `Composite: ALTER TABLE ${tableName} ADD INDEX idx_composite (${column}, other_column);`],
180
+ });
181
+ }));
182
+ mcp.registerTool('get_queue_details', {
202
183
  description: 'Returns all SQS queues with DLQ status, encryption, message counts, and retention.',
203
- inputSchema: { type: 'object', properties: {} },
204
- handler: async () => {
205
- const queues = (0, graph_1.getQueueNodes)(currentGraph);
206
- const queueFindings = currentFindings.filter((f) => f.metadata?.queueName);
207
- return toText({
208
- total: queues.length,
209
- queues: queues.map((q) => ({
210
- name: q.name, provider: q.provider, hasDLQ: q.hasDLQ, encrypted: q.encrypted,
211
- approximateMessages: q.approximateMessages, retentionDays: q.retentionDays,
212
- findings: queueFindings.filter((f) => f.metadata.queueName === q.name).map((f) => ({ severity: f.severity, issue: f.issue })),
213
- })),
214
- });
215
- },
216
- },
217
- {
218
- name: 'get_topic_details',
184
+ inputSchema: zod_1.z.object({}),
185
+ }, logged('get_queue_details', async () => {
186
+ const queues = (0, graph_1.getQueueNodes)(currentGraph);
187
+ const queueFindings = currentFindings.filter((f) => f.metadata?.queueName);
188
+ return toText({
189
+ total: queues.length,
190
+ queues: queues.map((q) => ({
191
+ name: q.name, provider: q.provider, hasDLQ: q.hasDLQ, encrypted: q.encrypted,
192
+ approximateMessages: q.approximateMessages, retentionDays: q.retentionDays,
193
+ findings: queueFindings.filter((f) => f.metadata.queueName === q.name).map((f) => ({ severity: f.severity, issue: f.issue })),
194
+ })),
195
+ });
196
+ }));
197
+ mcp.registerTool('get_topic_details', {
219
198
  description: 'Returns all SNS topics with subscription counts and protocols.',
220
- inputSchema: { type: 'object', properties: {} },
221
- handler: async () => {
222
- const topics = (0, graph_1.getTopicNodes)(currentGraph);
223
- return toText({ total: topics.length, topics: topics.map((t) => ({ name: t.name, provider: t.provider, subscriptionCount: t.subscriptionCount, encrypted: t.encrypted })) });
224
- },
225
- },
226
- {
227
- name: 'get_secrets_overview',
199
+ inputSchema: zod_1.z.object({}),
200
+ }, logged('get_topic_details', async () => {
201
+ const topics = (0, graph_1.getTopicNodes)(currentGraph);
202
+ return toText({ total: topics.length, topics: topics.map((t) => ({ name: t.name, provider: t.provider, subscriptionCount: t.subscriptionCount, encrypted: t.encrypted })) });
203
+ }));
204
+ mcp.registerTool('get_secrets_overview', {
228
205
  description: 'Returns all Secrets Manager secrets: names, rotation status. Secret VALUES are never included.',
229
- inputSchema: { type: 'object', properties: {} },
230
- handler: async () => {
231
- const secrets = (0, graph_1.getSecretNodes)(currentGraph);
232
- const secretFindings = currentFindings.filter((f) => f.metadata?.secretName);
233
- return toText({
234
- total: secrets.length, note: 'Secret values are never included in this response.',
235
- secrets: secrets.map((s) => ({
236
- name: s.name, provider: s.provider, rotationEnabled: s.rotationEnabled, rotationDays: s.rotationDays,
237
- findings: secretFindings.filter((f) => f.metadata.secretName === s.name).map((f) => ({ severity: f.severity, issue: f.issue })),
238
- })),
239
- });
240
- },
241
- },
242
- {
243
- name: 'get_parameter_overview',
206
+ inputSchema: zod_1.z.object({}),
207
+ }, logged('get_secrets_overview', async () => {
208
+ const secrets = (0, graph_1.getSecretNodes)(currentGraph);
209
+ const secretFindings = currentFindings.filter((f) => f.metadata?.secretName);
210
+ return toText({
211
+ total: secrets.length, note: 'Secret values are never included in this response.',
212
+ secrets: secrets.map((s) => ({
213
+ name: s.name, provider: s.provider, rotationEnabled: s.rotationEnabled, rotationDays: s.rotationDays,
214
+ findings: secretFindings.filter((f) => f.metadata.secretName === s.name).map((f) => ({ severity: f.severity, issue: f.issue })),
215
+ })),
216
+ });
217
+ }));
218
+ mcp.registerTool('get_parameter_overview', {
244
219
  description: 'Returns all SSM Parameter Store parameters: names, types, tiers. Parameter VALUES are never included.',
245
- inputSchema: { type: 'object', properties: {} },
246
- handler: async () => {
247
- const parameters = (0, graph_1.getParameterNodes)(currentGraph);
248
- return toText({
249
- total: parameters.length, note: 'Parameter values are never included in this response.',
250
- parameters: parameters.map((p) => ({ name: p.name, provider: p.provider, type: p.paramType, tier: p.tier })),
251
- });
252
- },
253
- },
254
- {
255
- name: 'get_lambda_overview',
220
+ inputSchema: zod_1.z.object({}),
221
+ }, logged('get_parameter_overview', async () => {
222
+ const parameters = (0, graph_1.getParameterNodes)(currentGraph);
223
+ return toText({
224
+ total: parameters.length, note: 'Parameter values are never included in this response.',
225
+ parameters: parameters.map((p) => ({ name: p.name, provider: p.provider, type: p.paramType, tier: p.tier })),
226
+ });
227
+ }));
228
+ mcp.registerTool('get_lambda_overview', {
256
229
  description: 'Returns all Lambda functions: runtime, memory, timeout, env var key names (values never included).',
257
- inputSchema: { type: 'object', properties: {} },
258
- handler: async () => {
259
- const lambdas = (0, graph_1.getLambdaNodes)(currentGraph);
260
- const lambdaFindings = currentFindings.filter((f) => f.metadata?.functionName);
261
- return toText({
262
- total: lambdas.length, note: 'Environment variable values are never included.',
263
- lambdas: lambdas.map((l) => ({
264
- name: l.name, runtime: l.runtime, memoryMB: l.memoryMB, timeoutSec: l.timeoutSec,
265
- envVarCount: l.envVarKeys?.length ?? 0, envVarKeys: l.envVarKeys,
266
- findings: lambdaFindings.filter((f) => f.metadata.functionName === l.name).map((f) => ({ severity: f.severity, issue: f.issue })),
267
- })),
268
- });
269
- },
270
- },
271
- {
272
- name: 'get_log_errors',
230
+ inputSchema: zod_1.z.object({}),
231
+ }, logged('get_lambda_overview', async () => {
232
+ const lambdas = (0, graph_1.getLambdaNodes)(currentGraph);
233
+ const lambdaFindings = currentFindings.filter((f) => f.metadata?.functionName);
234
+ return toText({
235
+ total: lambdas.length, note: 'Environment variable values are never included.',
236
+ lambdas: lambdas.map((l) => ({
237
+ name: l.name, runtime: l.runtime, memoryMB: l.memoryMB, timeoutSec: l.timeoutSec,
238
+ envVarCount: l.envVarKeys?.length ?? 0, envVarKeys: l.envVarKeys,
239
+ findings: lambdaFindings.filter((f) => f.metadata.functionName === l.name).map((f) => ({ severity: f.severity, issue: f.issue })),
240
+ })),
241
+ });
242
+ }));
243
+ mcp.registerTool('get_log_errors', {
273
244
  description: 'Returns recent error patterns from CloudWatch log groups. Returns pattern counts and frequencies — never raw log messages.',
274
- inputSchema: {
275
- type: 'object',
276
- properties: { logGroup: { type: 'string', description: 'Filter to a specific log group name (optional)' } },
277
- },
278
- handler: async ({ logGroup: filterName }) => {
279
- const logGroups = (0, graph_1.getLogGroupNodes)(currentGraph).filter((lg) => !filterName || lg.name.includes(String(filterName)));
280
- return toText({
281
- note: 'Only error patterns and counts are returned — no raw log messages.',
282
- windowHours: 24,
283
- logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never-expires', errorCount: lg.errorCount, topErrorPatterns: lg.topErrorPatterns })),
284
- });
285
- },
286
- },
287
- ];
288
- const ok = (id, result) => ({ jsonrpc: '2.0', id, result });
289
- const rpcErr = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
290
- async function handleMcp(body) {
291
- const { method, params = {}, id } = body;
292
- if (method === 'initialize') {
293
- return ok(id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'infrawise', version: '0.1.0' } });
294
- }
295
- if (method === 'notifications/initialized' || method === 'ping') {
296
- return id != null ? ok(id, {}) : null;
297
- }
298
- if (method === 'tools/list') {
299
- return ok(id, { tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })) });
300
- }
301
- if (method === 'tools/call') {
302
- const { name, arguments: args = {} } = params;
303
- const tool = TOOLS.find((t) => t.name === name);
304
- if (!tool)
305
- return rpcErr(id, -32601, `Unknown tool: ${name}`);
306
- core_1.logger.info(`→ ${name}${Object.keys(args).length ? ` ${JSON.stringify(args)}` : ''}`);
307
- try {
308
- return ok(id, await tool.handler(args));
309
- }
310
- catch (e) {
311
- return rpcErr(id, -32603, e instanceof Error ? e.message : String(e));
312
- }
313
- }
314
- return rpcErr(id, -32601, `Method not found: ${method}`);
245
+ inputSchema: zod_1.z.object({ logGroup: zod_1.z.string().describe('Filter to a specific log group name (optional)').optional() }),
246
+ }, logged('get_log_errors', async ({ logGroup: filterName }) => {
247
+ const logGroups = (0, graph_1.getLogGroupNodes)(currentGraph).filter((lg) => !filterName || lg.name.includes(filterName));
248
+ return toText({
249
+ note: 'Only error patterns and counts are returned — no raw log messages.',
250
+ windowHours: 24,
251
+ logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never-expires', errorCount: lg.errorCount, topErrorPatterns: lg.topErrorPatterns })),
252
+ });
253
+ }));
254
+ return mcp;
315
255
  }
316
256
  // ── Fastify server ────────────────────────────────────────────────────────────
317
257
  function createServer(port = 3000) {
318
258
  const fastify = (0, fastify_1.default)({ logger: false });
319
259
  fastify.register(cors_1.default, { origin: true });
260
+ const mcp = createMcpServer();
320
261
  fastify.get('/health', async () => ({
321
- status: 'ok', version: '0.1.0',
262
+ status: 'ok', version,
322
263
  graphNodes: currentGraph.nodes.length,
323
264
  graphEdges: currentGraph.edges.length,
324
265
  findings: currentFindings.length,
325
266
  }));
326
- fastify.get('/mcp/tools', async () => ({
327
- tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
328
- }));
329
267
  fastify.post('/mcp', async (request, reply) => {
330
- const response = await handleMcp(request.body);
331
- if (response === null)
332
- return reply.code(204).send();
333
- return response;
268
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
269
+ reply.raw.on('close', () => transport.close());
270
+ await mcp.connect(transport);
271
+ await transport.handleRequest(request.raw, reply.raw, request.body);
272
+ return reply;
334
273
  });
335
274
  return {
336
275
  fastify,
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "infrawise",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
+ "mcpName": "io.github.Sidd27/infrawise",
4
5
  "description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
5
6
  "keywords": [
6
7
  "infrastructure",
@@ -60,8 +61,9 @@
60
61
  "@aws-sdk/client-ssm": "^3.0.0",
61
62
  "@aws-sdk/credential-providers": "^3.0.0",
62
63
  "@fastify/cors": "^9.0.0",
64
+ "@modelcontextprotocol/sdk": "^1.29.0",
63
65
  "chalk": "^4.1.2",
64
- "commander": "^12.0.0",
66
+ "commander": "^14.0.3",
65
67
  "fastify": "^4.26.0",
66
68
  "inquirer": "^8.2.7",
67
69
  "js-yaml": "^4.1.0",