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 +27 -48
- package/dist/cli/commands/analyze.js +85 -82
- package/dist/cli/commands/dev.js +87 -7
- package/dist/cli/index.js +1 -6
- package/dist/core/config.js +1 -0
- package/dist/server/index.js +220 -281
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Infrawise
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
✔
|
|
108
|
-
✔
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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...'
|
|
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...'
|
|
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...'
|
|
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...'
|
|
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...'
|
|
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...'
|
|
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...'
|
|
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...'
|
|
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...'
|
|
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...'
|
|
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)...'
|
|
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)...'
|
|
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
|
|
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...'
|
|
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...'
|
|
255
|
+
const s = mkSpinner('Running analyzers...');
|
|
309
256
|
const analyzers = [
|
|
310
|
-
...(config.dynamodb?.enabled === true
|
|
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
|
|
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
|
|
267
|
+
...(config.mysql?.enabled ? [
|
|
321
268
|
new analyzers_1.MissingMySQLIndexAnalyzer(),
|
|
322
269
|
new analyzers_1.MySQLFullTableScanAnalyzer(),
|
|
323
270
|
] : []),
|
|
324
|
-
...(config.mongodb?.enabled
|
|
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
|
+
}
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
(0,
|
|
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
|
-
|
|
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
|
package/dist/core/config.js
CHANGED
|
@@ -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(),
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
name: '
|
|
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:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
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
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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.
|
|
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": "^
|
|
66
|
+
"commander": "^14.0.3",
|
|
65
67
|
"fastify": "^4.26.0",
|
|
66
68
|
"inquirer": "^8.2.7",
|
|
67
69
|
"js-yaml": "^4.1.0",
|