infrawise 0.8.2 → 0.8.7

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.
@@ -22,7 +22,12 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
22
22
  }
23
23
  for (const table of postgresMeta) {
24
24
  const nodeId = `table:postgres:${table.schema}.${table.table}`;
25
- addNode({ id: nodeId, type: 'table', name: `${table.schema}.${table.table}`, databaseType: 'postgres' });
25
+ addNode({
26
+ id: nodeId,
27
+ type: 'table',
28
+ name: `${table.schema}.${table.table}`,
29
+ databaseType: 'postgres',
30
+ });
26
31
  for (const indexName of table.indexes) {
27
32
  const indexNodeId = `index:${table.schema}.${table.table}:${indexName}`;
28
33
  addNode({ id: indexNodeId, type: 'index', name: indexName });
@@ -31,7 +36,12 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
31
36
  }
32
37
  for (const table of mysqlMeta) {
33
38
  const nodeId = `table:mysql:${table.schema}.${table.table}`;
34
- addNode({ id: nodeId, type: 'table', name: `${table.schema}.${table.table}`, databaseType: 'mysql' });
39
+ addNode({
40
+ id: nodeId,
41
+ type: 'table',
42
+ name: `${table.schema}.${table.table}`,
43
+ databaseType: 'mysql',
44
+ });
35
45
  for (const indexName of table.indexes) {
36
46
  const indexNodeId = `index:${table.schema}.${table.table}:${indexName}`;
37
47
  addNode({ id: indexNodeId, type: 'index', name: indexName });
@@ -40,7 +50,12 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
40
50
  }
41
51
  for (const coll of mongoMeta) {
42
52
  const nodeId = `table:mongodb:${coll.database}.${coll.collection}`;
43
- addNode({ id: nodeId, type: 'table', name: `${coll.database}.${coll.collection}`, databaseType: 'mongodb' });
53
+ addNode({
54
+ id: nodeId,
55
+ type: 'table',
56
+ name: `${coll.database}.${coll.collection}`,
57
+ databaseType: 'mongodb',
58
+ });
44
59
  for (const idx of coll.indexes) {
45
60
  if (idx.name === '_id_')
46
61
  continue;
@@ -122,15 +137,34 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
122
137
  let sourceId;
123
138
  if (trigger.type === 'sqs') {
124
139
  sourceId = `queue:aws:${trigger.sourceName}`;
125
- addNode({ id: sourceId, type: 'queue', name: trigger.sourceName, provider: 'aws', hasDLQ: false, encrypted: false });
140
+ addNode({
141
+ id: sourceId,
142
+ type: 'queue',
143
+ name: trigger.sourceName,
144
+ provider: 'aws',
145
+ hasDLQ: false,
146
+ encrypted: false,
147
+ });
126
148
  }
127
149
  else if (trigger.type === 'dynamodb') {
128
150
  sourceId = `table:dynamo:${trigger.sourceName}`;
129
- addNode({ id: sourceId, type: 'table', name: trigger.sourceName, databaseType: 'dynamodb' });
151
+ addNode({
152
+ id: sourceId,
153
+ type: 'table',
154
+ name: trigger.sourceName,
155
+ databaseType: 'dynamodb',
156
+ });
130
157
  }
131
158
  else if (trigger.type === 'kinesis') {
132
159
  sourceId = `queue:aws:${trigger.sourceName}`;
133
- addNode({ id: sourceId, type: 'queue', name: trigger.sourceName, provider: 'aws', hasDLQ: false, encrypted: false });
160
+ addNode({
161
+ id: sourceId,
162
+ type: 'queue',
163
+ name: trigger.sourceName,
164
+ provider: 'aws',
165
+ hasDLQ: false,
166
+ encrypted: false,
167
+ });
134
168
  }
135
169
  else {
136
170
  continue;
@@ -223,7 +257,14 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
223
257
  // AWS service operations create edges to service nodes
224
258
  if (op.serviceType === 'sqs') {
225
259
  const queueId = `queue:aws:${op.target}`;
226
- addNode({ id: queueId, type: 'queue', name: op.target, provider: 'aws', hasDLQ: false, encrypted: false });
260
+ addNode({
261
+ id: queueId,
262
+ type: 'queue',
263
+ name: op.target,
264
+ provider: 'aws',
265
+ hasDLQ: false,
266
+ encrypted: false,
267
+ });
227
268
  edges.push({ from: funcNodeId, to: queueId, type: 'publishes_to' });
228
269
  continue;
229
270
  }
@@ -242,13 +283,26 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
242
283
  }
243
284
  if (op.serviceType === 'ssm') {
244
285
  const paramId = `parameter:aws:${op.target}`;
245
- addNode({ id: paramId, type: 'parameter', name: op.target, provider: 'aws', paramType: 'String', tier: 'Standard' });
286
+ addNode({
287
+ id: paramId,
288
+ type: 'parameter',
289
+ name: op.target,
290
+ provider: 'aws',
291
+ paramType: 'String',
292
+ tier: 'Standard',
293
+ });
246
294
  edges.push({ from: funcNodeId, to: paramId, type: 'reads_parameter' });
247
295
  continue;
248
296
  }
249
297
  if (op.serviceType === 'secretsmanager') {
250
298
  const secretId = `secret:aws:${op.target}`;
251
- addNode({ id: secretId, type: 'secret', name: op.target, provider: 'aws', rotationEnabled: false });
299
+ addNode({
300
+ id: secretId,
301
+ type: 'secret',
302
+ name: op.target,
303
+ provider: 'aws',
304
+ rotationEnabled: false,
305
+ });
252
306
  edges.push({ from: funcNodeId, to: secretId, type: 'reads_secret' });
253
307
  continue;
254
308
  }
@@ -45,23 +45,43 @@ export function createMcpServer() {
45
45
  const buckets = getBucketNodes(currentGraph);
46
46
  return toText({
47
47
  summary: {
48
- tables: tables.length, functions: functions.length,
49
- queues: queues.length, topics: topics.length,
50
- secrets: secrets.length, parameters: parameters.length,
51
- logGroups: logGroups.length, lambdas: lambdas.length,
48
+ tables: tables.length,
49
+ functions: functions.length,
50
+ queues: queues.length,
51
+ topics: topics.length,
52
+ secrets: secrets.length,
53
+ parameters: parameters.length,
54
+ logGroups: logGroups.length,
55
+ lambdas: lambdas.length,
52
56
  buckets: buckets.length,
53
- totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
57
+ totalNodes: currentGraph.nodes.length,
58
+ totalEdges: currentGraph.edges.length,
54
59
  findings: summarizeFindings(currentFindings),
55
60
  },
56
61
  databases: tables.map((t) => ({ name: t.name, type: t.databaseType })),
57
- queues: queues.map((q) => ({ name: q.name, hasDLQ: q.hasDLQ, encrypted: q.encrypted, approximateMessages: q.approximateMessages })),
62
+ queues: queues.map((q) => ({
63
+ name: q.name,
64
+ hasDLQ: q.hasDLQ,
65
+ encrypted: q.encrypted,
66
+ approximateMessages: q.approximateMessages,
67
+ })),
58
68
  topics: topics.map((t) => ({ name: t.name, subscriptions: t.subscriptionCount })),
59
69
  secrets: secrets.map((s) => ({ name: s.name, rotationEnabled: s.rotationEnabled })),
60
70
  parameters: parameters.map((p) => ({ name: p.name, type: p.paramType, tier: p.tier })),
61
71
  lambdas: lambdas.map((l) => ({ name: l.name, runtime: l.runtime, memoryMB: l.memoryMB })),
62
- logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never', errorCount: lg.errorCount })),
63
- buckets: buckets.map((b) => ({ name: b.name, versioned: b.versioned, publicAccessBlocked: b.publicAccessBlocked })),
64
- highFindings: currentFindings.filter((f) => f.severity === 'high').map((f) => ({ issue: f.issue, recommendation: f.recommendation })),
72
+ logGroups: logGroups.map((lg) => ({
73
+ name: lg.name,
74
+ retentionDays: lg.retentionDays ?? 'never',
75
+ errorCount: lg.errorCount,
76
+ })),
77
+ buckets: buckets.map((b) => ({
78
+ name: b.name,
79
+ versioned: b.versioned,
80
+ publicAccessBlocked: b.publicAccessBlocked,
81
+ })),
82
+ highFindings: currentFindings
83
+ .filter((f) => f.severity === 'high')
84
+ .map((f) => ({ issue: f.issue, recommendation: f.recommendation })),
65
85
  });
66
86
  }));
67
87
  mcp.registerTool('get_graph_summary', {
@@ -72,9 +92,12 @@ export function createMcpServer() {
72
92
  edges: currentGraph.edges,
73
93
  findings: currentFindings,
74
94
  summary: {
75
- totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
76
- tables: getTableNodes(currentGraph).length, functions: getFunctionNodes(currentGraph).length,
77
- queues: getQueueNodes(currentGraph).length, scans: getScanEdges(currentGraph).length,
95
+ totalNodes: currentGraph.nodes.length,
96
+ totalEdges: currentGraph.edges.length,
97
+ tables: getTableNodes(currentGraph).length,
98
+ functions: getFunctionNodes(currentGraph).length,
99
+ queues: getQueueNodes(currentGraph).length,
100
+ scans: getScanEdges(currentGraph).length,
78
101
  ...summarizeFindings(currentFindings),
79
102
  },
80
103
  })));
@@ -86,26 +109,44 @@ export function createMcpServer() {
86
109
  // Also check if there's a Lambda node with this name (for AWS-deployed functions)
87
110
  const lambdaNode = currentGraph.nodes.find((n) => n.type === 'lambda' && n.name === functionName);
88
111
  if (!funcNode && !lambdaNode) {
89
- return toText({ function: functionName, found: false, issues: [], recommendations: [`Function "${functionName}" not found in the analyzed codebase.`] });
112
+ return toText({
113
+ function: functionName,
114
+ found: false,
115
+ issues: [],
116
+ recommendations: [`Function "${functionName}" not found in the analyzed codebase.`],
117
+ });
90
118
  }
91
119
  const outEdges = funcNode ? getOutgoingEdges(currentGraph, funcNode.id) : [];
92
120
  const relatedFindings = currentFindings.filter((f) => {
93
121
  const meta = f.metadata;
94
- return meta?.functionName === functionName || String(meta?.callerFunctions ?? '').includes(functionName);
122
+ return (meta?.functionName === functionName ||
123
+ String(meta?.callerFunctions ?? '').includes(functionName));
95
124
  });
96
125
  const allTriggers = lambdaNode?.type === 'lambda' ? (lambdaNode.triggers ?? []) : [];
97
126
  return toText({
98
- function: functionName, found: true,
127
+ function: functionName,
128
+ found: true,
99
129
  file: funcNode?.type === 'function' ? funcNode.file : undefined,
100
130
  triggers: allTriggers.map((t) => ({
101
- type: t.type, source: t.sourceName, eventShape: t.eventShape,
131
+ type: t.type,
132
+ source: t.sourceName,
133
+ eventShape: t.eventShape,
102
134
  ...(t.ruleName ? { ruleName: t.ruleName, eventPattern: t.eventPattern } : {}),
103
135
  })),
104
136
  accesses: outEdges.map((e) => {
105
137
  const target = currentGraph.nodes.find((n) => n.id === e.to);
106
- return { targetId: e.to, edgeType: e.type, targetName: target && 'name' in target ? target.name : e.to, targetType: target?.type };
138
+ return {
139
+ targetId: e.to,
140
+ edgeType: e.type,
141
+ targetName: target && 'name' in target ? target.name : e.to,
142
+ targetType: target?.type,
143
+ };
107
144
  }),
108
- issues: relatedFindings.map((f) => ({ severity: f.severity, issue: f.issue, description: f.description })),
145
+ issues: relatedFindings.map((f) => ({
146
+ severity: f.severity,
147
+ issue: f.issue,
148
+ description: f.description,
149
+ })),
109
150
  recommendations: [...new Set(relatedFindings.map((f) => f.recommendation))],
110
151
  });
111
152
  }));
@@ -119,10 +160,20 @@ export function createMcpServer() {
119
160
  const sanitizedAttr = attribute.replace(/[^a-zA-Z0-9_]/g, '_');
120
161
  const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_-]/g, '_');
121
162
  const indexName = `${sanitizedTable}-${sanitizedAttr}-index`;
122
- const tableNode = currentGraph.nodes.find((n) => n.type === 'table' && n.databaseType === 'dynamodb' && 'name' in n && n.name === tableName);
163
+ const tableNode = currentGraph.nodes.find((n) => n.type === 'table' &&
164
+ n.databaseType === 'dynamodb' &&
165
+ 'name' in n &&
166
+ n.name === tableName);
123
167
  return toText({
124
- table: tableName, attribute, found: !!tableNode,
125
- index: { name: indexName, partitionKey: attribute, projectionType: 'ALL', billingMode: 'PAY_PER_REQUEST' },
168
+ table: tableName,
169
+ attribute,
170
+ found: !!tableNode,
171
+ index: {
172
+ name: indexName,
173
+ partitionKey: attribute,
174
+ projectionType: 'ALL',
175
+ billingMode: 'PAY_PER_REQUEST',
176
+ },
126
177
  rationale: `A GSI on "${attribute}" allows Query instead of Scan when filtering by this attribute.`,
127
178
  recommendation: `Add GSI "${indexName}" with partition key "${attribute}" to your IaC definition.`,
128
179
  });
@@ -138,11 +189,15 @@ export function createMcpServer() {
138
189
  const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_]/g, '_');
139
190
  const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
140
191
  return toText({
141
- table: tableName, column,
192
+ table: tableName,
193
+ column,
142
194
  recommendation: `CREATE INDEX CONCURRENTLY ${indexName} ON ${sanitizedTable} (${sanitizedCol});`,
143
195
  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 ${sanitizedTable} (${sanitizedCol}) WHERE ${sanitizedCol} IS NOT NULL;`],
196
+ notes: [
197
+ 'Use CONCURRENTLY to avoid locking the table',
198
+ 'Run ANALYZE after creation',
199
+ `Partial index: CREATE INDEX CONCURRENTLY ${indexName}_partial ON ${sanitizedTable} (${sanitizedCol}) WHERE ${sanitizedCol} IS NOT NULL;`,
200
+ ],
146
201
  });
147
202
  }));
148
203
  mcp.registerTool('suggest_mongo_index', {
@@ -155,7 +210,8 @@ export function createMcpServer() {
155
210
  const sanitizedCollection = collection.replace(/[^a-zA-Z0-9_]/g, '_');
156
211
  const sanitizedField = field.replace(/[^a-zA-Z0-9_.]/g, '_');
157
212
  return toText({
158
- collection, field,
213
+ collection,
214
+ field,
159
215
  recommendation: `db.${sanitizedCollection}.createIndex({ ${sanitizedField}: 1 })`,
160
216
  rationale: `An index on "${field}" eliminates full collection scans when filtering on this field.`,
161
217
  notes: [
@@ -176,11 +232,15 @@ export function createMcpServer() {
176
232
  const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_]/g, '_');
177
233
  const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
178
234
  return toText({
179
- table: tableName, column,
235
+ table: tableName,
236
+ column,
180
237
  recommendation: `ALTER TABLE ${sanitizedTable} ADD INDEX ${indexName} (${sanitizedCol});`,
181
238
  rationale: `An index on "${column}" eliminates full table scans when filtering on this column.`,
182
- notes: ['MySQL InnoDB adds indexes online (no full lock for 5.6+)', 'EXPLAIN SELECT ... to verify after adding',
183
- `Composite: ALTER TABLE ${sanitizedTable} ADD INDEX idx_composite (${sanitizedCol}, other_column);`],
239
+ notes: [
240
+ 'MySQL InnoDB adds indexes online (no full lock for 5.6+)',
241
+ 'EXPLAIN SELECT ... to verify after adding',
242
+ `Composite: ALTER TABLE ${sanitizedTable} ADD INDEX idx_composite (${sanitizedCol}, other_column);`,
243
+ ],
184
244
  });
185
245
  }));
186
246
  mcp.registerTool('get_queue_details', {
@@ -192,9 +252,15 @@ export function createMcpServer() {
192
252
  return toText({
193
253
  total: queues.length,
194
254
  queues: queues.map((q) => ({
195
- name: q.name, provider: q.provider, hasDLQ: q.hasDLQ, encrypted: q.encrypted,
196
- approximateMessages: q.approximateMessages, retentionDays: q.retentionDays,
197
- findings: queueFindings.filter((f) => f.metadata.queueName === q.name).map((f) => ({ severity: f.severity, issue: f.issue })),
255
+ name: q.name,
256
+ provider: q.provider,
257
+ hasDLQ: q.hasDLQ,
258
+ encrypted: q.encrypted,
259
+ approximateMessages: q.approximateMessages,
260
+ retentionDays: q.retentionDays,
261
+ findings: queueFindings
262
+ .filter((f) => f.metadata.queueName === q.name)
263
+ .map((f) => ({ severity: f.severity, issue: f.issue })),
198
264
  })),
199
265
  });
200
266
  }));
@@ -203,7 +269,15 @@ export function createMcpServer() {
203
269
  inputSchema: z.object({}),
204
270
  }, logged('get_topic_details', async () => {
205
271
  const topics = getTopicNodes(currentGraph);
206
- return toText({ total: topics.length, topics: topics.map((t) => ({ name: t.name, provider: t.provider, subscriptionCount: t.subscriptionCount, encrypted: t.encrypted })) });
272
+ return toText({
273
+ total: topics.length,
274
+ topics: topics.map((t) => ({
275
+ name: t.name,
276
+ provider: t.provider,
277
+ subscriptionCount: t.subscriptionCount,
278
+ encrypted: t.encrypted,
279
+ })),
280
+ });
207
281
  }));
208
282
  mcp.registerTool('get_secrets_overview', {
209
283
  description: 'Returns all Secrets Manager secrets with rotation status and rotation interval. Secret values are never returned. Call this when checking which secrets exist, confirming rotation is enabled before a security review, or identifying secrets that lack rotation.',
@@ -212,10 +286,16 @@ export function createMcpServer() {
212
286
  const secrets = getSecretNodes(currentGraph);
213
287
  const secretFindings = currentFindings.filter((f) => f.metadata?.secretName);
214
288
  return toText({
215
- total: secrets.length, note: 'Secret values are never included in this response.',
289
+ total: secrets.length,
290
+ note: 'Secret values are never included in this response.',
216
291
  secrets: secrets.map((s) => ({
217
- name: s.name, provider: s.provider, rotationEnabled: s.rotationEnabled, rotationDays: s.rotationDays,
218
- findings: secretFindings.filter((f) => f.metadata.secretName === s.name).map((f) => ({ severity: f.severity, issue: f.issue })),
292
+ name: s.name,
293
+ provider: s.provider,
294
+ rotationEnabled: s.rotationEnabled,
295
+ rotationDays: s.rotationDays,
296
+ findings: secretFindings
297
+ .filter((f) => f.metadata.secretName === s.name)
298
+ .map((f) => ({ severity: f.severity, issue: f.issue })),
219
299
  })),
220
300
  });
221
301
  }));
@@ -225,8 +305,14 @@ export function createMcpServer() {
225
305
  }, logged('get_parameter_overview', async () => {
226
306
  const parameters = getParameterNodes(currentGraph);
227
307
  return toText({
228
- total: parameters.length, note: 'Parameter values are never included in this response.',
229
- parameters: parameters.map((p) => ({ name: p.name, provider: p.provider, type: p.paramType, tier: p.tier })),
308
+ total: parameters.length,
309
+ note: 'Parameter values are never included in this response.',
310
+ parameters: parameters.map((p) => ({
311
+ name: p.name,
312
+ provider: p.provider,
313
+ type: p.paramType,
314
+ tier: p.tier,
315
+ })),
230
316
  });
231
317
  }));
232
318
  mcp.registerTool('get_lambda_overview', {
@@ -236,12 +322,24 @@ export function createMcpServer() {
236
322
  const lambdas = getLambdaNodes(currentGraph);
237
323
  const lambdaFindings = currentFindings.filter((f) => f.metadata?.functionName);
238
324
  return toText({
239
- total: lambdas.length, note: 'Environment variable values are never included.',
325
+ total: lambdas.length,
326
+ note: 'Environment variable values are never included.',
240
327
  lambdas: lambdas.map((l) => ({
241
- name: l.name, runtime: l.runtime, memoryMB: l.memoryMB, timeoutSec: l.timeoutSec,
242
- envVarCount: l.envVarKeys?.length ?? 0, envVarKeys: l.envVarKeys,
243
- triggers: (l.triggers ?? []).map((t) => ({ type: t.type, source: t.sourceName, eventShape: t.eventShape, state: t.state })),
244
- findings: lambdaFindings.filter((f) => f.metadata.functionName === l.name).map((f) => ({ severity: f.severity, issue: f.issue })),
328
+ name: l.name,
329
+ runtime: l.runtime,
330
+ memoryMB: l.memoryMB,
331
+ timeoutSec: l.timeoutSec,
332
+ envVarCount: l.envVarKeys?.length ?? 0,
333
+ envVarKeys: l.envVarKeys,
334
+ triggers: (l.triggers ?? []).map((t) => ({
335
+ type: t.type,
336
+ source: t.sourceName,
337
+ eventShape: t.eventShape,
338
+ state: t.state,
339
+ })),
340
+ findings: lambdaFindings
341
+ .filter((f) => f.metadata.functionName === l.name)
342
+ .map((f) => ({ severity: f.severity, issue: f.issue })),
245
343
  })),
246
344
  });
247
345
  }));
@@ -261,7 +359,7 @@ export function createMcpServer() {
261
359
  .filter((e) => e.from === r.id && e.type === 'triggers')
262
360
  .map((e) => currentGraph.nodes.find((n) => n.id === e.to))
263
361
  .filter(Boolean)
264
- .map((n) => n && 'name' in n ? n.name : ''),
362
+ .map((n) => (n && 'name' in n ? n.name : '')),
265
363
  })),
266
364
  });
267
365
  }));
@@ -288,13 +386,20 @@ export function createMcpServer() {
288
386
  }));
289
387
  mcp.registerTool('get_log_errors', {
290
388
  description: 'Returns recent error pattern summaries from CloudWatch log groups: pattern counts and frequencies grouped by log group. Raw log messages are never returned. Use the optional logGroup filter to scope to one group by name substring. Call this when investigating errors or identifying log groups with no retention policy.',
291
- inputSchema: z.object({ logGroup: z.string().describe('Filter to a specific log group name (optional)').optional() }),
389
+ inputSchema: z.object({
390
+ logGroup: z.string().describe('Filter to a specific log group name (optional)').optional(),
391
+ }),
292
392
  }, logged('get_log_errors', async ({ logGroup: filterName }) => {
293
393
  const logGroups = getLogGroupNodes(currentGraph).filter((lg) => !filterName || lg.name.includes(filterName));
294
394
  return toText({
295
395
  note: 'Only error patterns and counts are returned — no raw log messages.',
296
396
  windowHours: 24,
297
- logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never-expires', errorCount: lg.errorCount, topErrorPatterns: lg.topErrorPatterns })),
397
+ logGroups: logGroups.map((lg) => ({
398
+ name: lg.name,
399
+ retentionDays: lg.retentionDays ?? 'never-expires',
400
+ errorCount: lg.errorCount,
401
+ topErrorPatterns: lg.topErrorPatterns,
402
+ })),
298
403
  });
299
404
  }));
300
405
  return mcp;
@@ -303,9 +408,9 @@ export function createMcpServer() {
303
408
  export function createServer(port = 3000) {
304
409
  const fastify = Fastify({ logger: false });
305
410
  fastify.register(cors, { origin: true });
306
- const mcp = createMcpServer();
307
411
  fastify.get('/health', async () => ({
308
- status: 'ok', version,
412
+ status: 'ok',
413
+ version,
309
414
  graphNodes: currentGraph.nodes.length,
310
415
  graphEdges: currentGraph.edges.length,
311
416
  findings: currentFindings.length,
@@ -320,13 +425,27 @@ export function createServer(port = 3000) {
320
425
  repository: 'https://github.com/Sidd27/infrawise',
321
426
  transports: [{ type: 'streamable-http', url: `http://localhost:${port}/mcp` }],
322
427
  tools: [
323
- 'get_infra_overview', 'get_graph_summary', 'analyze_function',
324
- 'suggest_gsi', 'postgres_index_suggestions', 'suggest_mongo_index', 'mysql_index_suggestions',
325
- 'get_queue_details', 'get_topic_details', 'get_secrets_overview', 'get_parameter_overview',
326
- 'get_lambda_overview', 'get_eventbridge_details', 'get_s3_overview', 'get_log_errors',
428
+ 'get_infra_overview',
429
+ 'get_graph_summary',
430
+ 'analyze_function',
431
+ 'suggest_gsi',
432
+ 'postgres_index_suggestions',
433
+ 'suggest_mongo_index',
434
+ 'mysql_index_suggestions',
435
+ 'get_queue_details',
436
+ 'get_topic_details',
437
+ 'get_secrets_overview',
438
+ 'get_parameter_overview',
439
+ 'get_lambda_overview',
440
+ 'get_eventbridge_details',
441
+ 'get_s3_overview',
442
+ 'get_log_errors',
327
443
  ],
328
444
  }));
329
445
  fastify.post('/mcp', async (request, reply) => {
446
+ // Fresh McpServer per request: connect() is one-shot per instance and throws if called
447
+ // on a live server, so a shared instance breaks under concurrent requests.
448
+ const mcp = createMcpServer();
330
449
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
331
450
  reply.raw.on('close', () => transport.close());
332
451
  await mcp.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infrawise",
3
- "version": "0.8.2",
3
+ "version": "0.8.7",
4
4
  "mcpName": "io.github.Sidd27/infrawise",
5
5
  "description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, S3, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
6
6
  "keywords": [
@@ -59,24 +59,30 @@
59
59
  "coverage": "vitest run --coverage",
60
60
  "lint": "eslint src",
61
61
  "typecheck": "tsc",
62
+ "format": "prettier --write src",
63
+ "generate-diagrams": "mmdc -i docs/architecture.mmd -o docs/architecture.svg --backgroundColor transparent",
62
64
  "dev": "node dist/cli/index.js dev",
63
- "release": "node scripts/release.js"
65
+ "release": "node scripts/release.js",
66
+ "prepare": "simple-git-hooks"
67
+ },
68
+ "simple-git-hooks": {
69
+ "pre-commit": "pnpm format && git add src/ && pnpm lint && pnpm typecheck && pnpm test"
64
70
  },
65
71
  "dependencies": {
66
72
  "@aws-sdk/client-cloudwatch-logs": "^3.1048.0",
67
- "@aws-sdk/client-s3": "^3.1048.0",
68
73
  "@aws-sdk/client-dynamodb": "^3.1048.0",
69
74
  "@aws-sdk/client-eventbridge": "^3.1051.0",
70
75
  "@aws-sdk/client-lambda": "^3.1048.0",
71
76
  "@aws-sdk/client-rds": "^3.1048.0",
77
+ "@aws-sdk/client-s3": "^3.1048.0",
72
78
  "@aws-sdk/client-secrets-manager": "^3.1048.0",
73
79
  "@aws-sdk/client-sns": "^3.1048.0",
74
80
  "@aws-sdk/client-sqs": "^3.1048.0",
75
81
  "@aws-sdk/client-ssm": "^3.1048.0",
76
82
  "@aws-sdk/credential-providers": "^3.1048.0",
83
+ "@fastify/cors": "^11.2.0",
77
84
  "@modelcontextprotocol/sdk": "^1.29.0",
78
85
  "chalk": "^5.6.2",
79
- "@fastify/cors": "^11.2.0",
80
86
  "commander": "^15.0.0",
81
87
  "fastify": "^5.8.5",
82
88
  "inquirer": "^14.0.2",
@@ -91,6 +97,7 @@
91
97
  "zod": "^4.4.3"
92
98
  },
93
99
  "devDependencies": {
100
+ "@mermaid-js/mermaid-cli": "^11.15.0",
94
101
  "@types/inquirer": "^9.0.9",
95
102
  "@types/js-yaml": "^4.0.9",
96
103
  "@types/node": "^25.8.0",
@@ -99,6 +106,8 @@
99
106
  "@typescript-eslint/parser": "^8.59.3",
100
107
  "@vitest/coverage-v8": "^4.1.6",
101
108
  "eslint": "^10.4.0",
109
+ "prettier": "^3.8.3",
110
+ "simple-git-hooks": "^2.13.1",
102
111
  "typescript": "^6.0.3",
103
112
  "vite": "^8.0.13",
104
113
  "vitest": "^4.1.6"
@@ -113,6 +122,10 @@
113
122
  "pnpm": {
114
123
  "overrides": {
115
124
  "qs": ">=6.15.2"
116
- }
125
+ },
126
+ "allowedBuildScripts": [
127
+ "puppeteer",
128
+ "simple-git-hooks"
129
+ ]
117
130
  }
118
131
  }