infrawise 0.4.2 → 0.6.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 +21 -48
- package/dist/adapters/aws.js +50 -64
- package/dist/adapters/dynamodb.js +17 -22
- package/dist/adapters/logs.js +12 -16
- package/dist/adapters/mongodb.js +10 -16
- package/dist/adapters/mysql.js +8 -17
- package/dist/adapters/postgres.js +9 -13
- package/dist/adapters/terraform.js +14 -56
- package/dist/analyzers/aws-services.js +7 -17
- package/dist/analyzers/dynamodb.js +6 -12
- package/dist/analyzers/index.js +13 -41
- package/dist/analyzers/mongodb.js +4 -9
- package/dist/analyzers/mysql.js +4 -9
- package/dist/analyzers/postgres.js +3 -9
- package/dist/analyzers/rds.js +5 -13
- package/dist/analyzers/terraform.js +1 -5
- package/dist/cli/commands/analyze.js +178 -215
- package/dist/cli/commands/auth.js +24 -30
- package/dist/cli/commands/dev.js +84 -43
- package/dist/cli/commands/doctor.js +35 -74
- package/dist/cli/commands/init.js +33 -72
- package/dist/cli/index.js +21 -28
- package/dist/cli/utils.js +40 -86
- package/dist/context/index.js +49 -85
- package/dist/core/cache.js +5 -43
- package/dist/core/config.js +43 -81
- package/dist/core/errors.js +7 -17
- package/dist/core/index.js +4 -22
- package/dist/core/logger.js +4 -10
- package/dist/graph/index.js +15 -32
- package/dist/server/index.js +236 -302
- package/dist/types.js +1 -2
- package/package.json +34 -31
package/dist/server/index.js
CHANGED
|
@@ -1,348 +1,282 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const analyzers_1 = require("../analyzers");
|
|
13
|
-
const graph_1 = require("../graph");
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import Fastify from 'fastify';
|
|
4
|
+
import cors from '@fastify/cors';
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { logger } from '../core/index.js';
|
|
9
|
+
const { version } = JSON.parse(readFileSync(join(import.meta.dirname, '../../package.json'), 'utf8'));
|
|
10
|
+
import { summarizeFindings } from '../analyzers/index.js';
|
|
11
|
+
import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
|
|
14
12
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
15
13
|
let currentGraph = { nodes: [], edges: [] };
|
|
16
|
-
exports.currentGraph = currentGraph;
|
|
17
14
|
let currentFindings = [];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
exports.currentFindings = currentFindings = findings;
|
|
15
|
+
export function setGraphState(graph, findings) {
|
|
16
|
+
currentGraph = graph;
|
|
17
|
+
currentFindings = findings;
|
|
22
18
|
}
|
|
19
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
23
20
|
function toText(data) {
|
|
24
21
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
25
22
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
name: '
|
|
23
|
+
function logged(name, fn) {
|
|
24
|
+
return async (args) => {
|
|
25
|
+
const hasArgs = Object.keys(args).length > 0;
|
|
26
|
+
logger.info(`→ ${name}${hasArgs ? ` ${JSON.stringify(args)}` : ''}`);
|
|
27
|
+
return fn(args);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// ── MCP Server ────────────────────────────────────────────────────────────────
|
|
31
|
+
export function createMcpServer() {
|
|
32
|
+
const mcp = new McpServer({ name: 'infrawise', version });
|
|
33
|
+
mcp.registerTool('get_infra_overview', {
|
|
30
34
|
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,
|
|
35
|
+
inputSchema: z.object({}),
|
|
36
|
+
}, logged('get_infra_overview', async () => {
|
|
37
|
+
const tables = getTableNodes(currentGraph);
|
|
38
|
+
const queues = getQueueNodes(currentGraph);
|
|
39
|
+
const topics = getTopicNodes(currentGraph);
|
|
40
|
+
const secrets = getSecretNodes(currentGraph);
|
|
41
|
+
const parameters = getParameterNodes(currentGraph);
|
|
42
|
+
const logGroups = getLogGroupNodes(currentGraph);
|
|
43
|
+
const lambdas = getLambdaNodes(currentGraph);
|
|
44
|
+
const functions = getFunctionNodes(currentGraph);
|
|
45
|
+
return toText({
|
|
69
46
|
summary: {
|
|
47
|
+
tables: tables.length, functions: functions.length,
|
|
48
|
+
queues: queues.length, topics: topics.length,
|
|
49
|
+
secrets: secrets.length, parameters: parameters.length,
|
|
50
|
+
logGroups: logGroups.length, lambdas: lambdas.length,
|
|
70
51
|
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),
|
|
52
|
+
findings: summarizeFindings(currentFindings),
|
|
74
53
|
},
|
|
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
|
-
});
|
|
54
|
+
databases: tables.map((t) => ({ name: t.name, type: t.databaseType })),
|
|
55
|
+
queues: queues.map((q) => ({ name: q.name, hasDLQ: q.hasDLQ, encrypted: q.encrypted, approximateMessages: q.approximateMessages })),
|
|
56
|
+
topics: topics.map((t) => ({ name: t.name, subscriptions: t.subscriptionCount })),
|
|
57
|
+
secrets: secrets.map((s) => ({ name: s.name, rotationEnabled: s.rotationEnabled })),
|
|
58
|
+
parameters: parameters.map((p) => ({ name: p.name, type: p.paramType, tier: p.tier })),
|
|
59
|
+
lambdas: lambdas.map((l) => ({ name: l.name, runtime: l.runtime, memoryMB: l.memoryMB })),
|
|
60
|
+
logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never', errorCount: lg.errorCount })),
|
|
61
|
+
highFindings: currentFindings.filter((f) => f.severity === 'high').map((f) => ({ issue: f.issue, recommendation: f.recommendation })),
|
|
62
|
+
});
|
|
63
|
+
}));
|
|
64
|
+
mcp.registerTool('get_graph_summary', {
|
|
65
|
+
description: 'Returns the full infrastructure graph (all nodes and edges) plus findings summary.',
|
|
66
|
+
inputSchema: z.object({}),
|
|
67
|
+
}, logged('get_graph_summary', async () => toText({
|
|
68
|
+
nodes: currentGraph.nodes,
|
|
69
|
+
edges: currentGraph.edges,
|
|
70
|
+
findings: currentFindings,
|
|
71
|
+
summary: {
|
|
72
|
+
totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
|
|
73
|
+
tables: getTableNodes(currentGraph).length, functions: getFunctionNodes(currentGraph).length,
|
|
74
|
+
queues: getQueueNodes(currentGraph).length, scans: getScanEdges(currentGraph).length,
|
|
75
|
+
...summarizeFindings(currentFindings),
|
|
105
76
|
},
|
|
106
|
-
}
|
|
107
|
-
{
|
|
108
|
-
|
|
77
|
+
})));
|
|
78
|
+
mcp.registerTool('analyze_function', {
|
|
79
|
+
description: 'Analyze a specific function for all infrastructure issues: DB queries, queue publishing, secret access, etc.',
|
|
80
|
+
inputSchema: z.object({ function: z.string().describe('Function name to analyze') }),
|
|
81
|
+
}, logged('analyze_function', async ({ function: functionName }) => {
|
|
82
|
+
const funcNode = currentGraph.nodes.find((n) => n.type === 'function' && n.name === functionName);
|
|
83
|
+
if (!funcNode) {
|
|
84
|
+
return toText({ function: functionName, found: false, issues: [], recommendations: [`Function "${functionName}" not found in the analyzed codebase.`] });
|
|
85
|
+
}
|
|
86
|
+
const outEdges = getOutgoingEdges(currentGraph, funcNode.id);
|
|
87
|
+
const relatedFindings = currentFindings.filter((f) => {
|
|
88
|
+
const meta = f.metadata;
|
|
89
|
+
return meta?.functionName === functionName || String(meta?.callerFunctions ?? '').includes(functionName);
|
|
90
|
+
});
|
|
91
|
+
return toText({
|
|
92
|
+
function: functionName, found: true,
|
|
93
|
+
file: funcNode.type === 'function' ? funcNode.file : undefined,
|
|
94
|
+
accesses: outEdges.map((e) => {
|
|
95
|
+
const target = currentGraph.nodes.find((n) => n.id === e.to);
|
|
96
|
+
return { targetId: e.to, edgeType: e.type, targetName: target && 'name' in target ? target.name : e.to, targetType: target?.type };
|
|
97
|
+
}),
|
|
98
|
+
issues: relatedFindings.map((f) => ({ severity: f.severity, issue: f.issue, description: f.description })),
|
|
99
|
+
recommendations: [...new Set(relatedFindings.map((f) => f.recommendation))],
|
|
100
|
+
});
|
|
101
|
+
}));
|
|
102
|
+
mcp.registerTool('suggest_gsi', {
|
|
109
103
|
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
|
-
|
|
127
|
-
});
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
name: 'postgres_index_suggestions',
|
|
104
|
+
inputSchema: z.object({
|
|
105
|
+
table: z.string().describe('DynamoDB table name'),
|
|
106
|
+
attribute: z.string().describe('Attribute to create the GSI on'),
|
|
107
|
+
}),
|
|
108
|
+
}, logged('suggest_gsi', async ({ table: tableName, attribute }) => {
|
|
109
|
+
const sanitizedAttr = attribute.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
110
|
+
const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
111
|
+
const indexName = `${sanitizedTable}-${sanitizedAttr}-index`;
|
|
112
|
+
const tableNode = currentGraph.nodes.find((n) => n.type === 'table' && n.databaseType === 'dynamodb' && 'name' in n && n.name === tableName);
|
|
113
|
+
return toText({
|
|
114
|
+
table: tableName, attribute, found: !!tableNode,
|
|
115
|
+
index: { name: indexName, partitionKey: attribute, projectionType: 'ALL', billingMode: 'PAY_PER_REQUEST' },
|
|
116
|
+
rationale: `A GSI on "${attribute}" allows Query instead of Scan when filtering by this attribute.`,
|
|
117
|
+
recommendation: `Add GSI "${indexName}" with partition key "${attribute}" to your IaC definition.`,
|
|
118
|
+
});
|
|
119
|
+
}));
|
|
120
|
+
mcp.registerTool('postgres_index_suggestions', {
|
|
132
121
|
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',
|
|
122
|
+
inputSchema: z.object({
|
|
123
|
+
table: z.string().describe('PostgreSQL table name'),
|
|
124
|
+
column: z.string().describe('Column name to index'),
|
|
125
|
+
}),
|
|
126
|
+
}, logged('postgres_index_suggestions', async ({ table: tableName, column }) => {
|
|
127
|
+
const sanitizedCol = column.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
128
|
+
const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
129
|
+
const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
|
|
130
|
+
return toText({
|
|
131
|
+
table: tableName, column,
|
|
132
|
+
recommendation: `CREATE INDEX CONCURRENTLY ${indexName} ON ${sanitizedTable} (${sanitizedCol});`,
|
|
133
|
+
rationale: `An index on "${column}" eliminates sequential scans when filtering on this column.`,
|
|
134
|
+
notes: ['Use CONCURRENTLY to avoid locking the table', 'Run ANALYZE after creation',
|
|
135
|
+
`Partial index: CREATE INDEX CONCURRENTLY ${indexName}_partial ON ${sanitizedTable} (${sanitizedCol}) WHERE ${sanitizedCol} IS NOT NULL;`],
|
|
136
|
+
});
|
|
137
|
+
}));
|
|
138
|
+
mcp.registerTool('suggest_mongo_index', {
|
|
156
139
|
description: 'Get index suggestions for a MongoDB collection field',
|
|
157
|
-
inputSchema: {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
handler: async ({ collection, field }) => toText({
|
|
140
|
+
inputSchema: z.object({
|
|
141
|
+
collection: z.string().describe('MongoDB collection name'),
|
|
142
|
+
field: z.string().describe('Field name to index'),
|
|
143
|
+
}),
|
|
144
|
+
}, logged('suggest_mongo_index', async ({ collection, field }) => {
|
|
145
|
+
const sanitizedCollection = collection.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
146
|
+
const sanitizedField = field.replace(/[^a-zA-Z0-9_.]/g, '_');
|
|
147
|
+
return toText({
|
|
166
148
|
collection, field,
|
|
167
|
-
recommendation: `db.${
|
|
168
|
-
rationale: `An index on "${
|
|
149
|
+
recommendation: `db.${sanitizedCollection}.createIndex({ ${sanitizedField}: 1 })`,
|
|
150
|
+
rationale: `An index on "${field}" eliminates full collection scans when filtering on this field.`,
|
|
169
151
|
notes: [
|
|
170
|
-
`Compound: db.${
|
|
171
|
-
`Text: db.${
|
|
172
|
-
`Verify: db.${
|
|
152
|
+
`Compound: db.${sanitizedCollection}.createIndex({ ${sanitizedField}: 1, otherField: 1 })`,
|
|
153
|
+
`Text: db.${sanitizedCollection}.createIndex({ ${sanitizedField}: "text" })`,
|
|
154
|
+
`Verify: db.${sanitizedCollection}.explain("executionStats").find({ ${sanitizedField}: value })`,
|
|
173
155
|
],
|
|
174
|
-
})
|
|
175
|
-
}
|
|
176
|
-
{
|
|
177
|
-
name: 'mysql_index_suggestions',
|
|
156
|
+
});
|
|
157
|
+
}));
|
|
158
|
+
mcp.registerTool('mysql_index_suggestions', {
|
|
178
159
|
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',
|
|
160
|
+
inputSchema: z.object({
|
|
161
|
+
table: z.string().describe('MySQL table name'),
|
|
162
|
+
column: z.string().describe('Column name to index'),
|
|
163
|
+
}),
|
|
164
|
+
}, logged('mysql_index_suggestions', async ({ table: tableName, column }) => {
|
|
165
|
+
const sanitizedCol = column.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
166
|
+
const sanitizedTable = tableName.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
167
|
+
const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
|
|
168
|
+
return toText({
|
|
169
|
+
table: tableName, column,
|
|
170
|
+
recommendation: `ALTER TABLE ${sanitizedTable} ADD INDEX ${indexName} (${sanitizedCol});`,
|
|
171
|
+
rationale: `An index on "${column}" eliminates full table scans when filtering on this column.`,
|
|
172
|
+
notes: ['MySQL InnoDB adds indexes online (no full lock for 5.6+)', 'EXPLAIN SELECT ... to verify after adding',
|
|
173
|
+
`Composite: ALTER TABLE ${sanitizedTable} ADD INDEX idx_composite (${sanitizedCol}, other_column);`],
|
|
174
|
+
});
|
|
175
|
+
}));
|
|
176
|
+
mcp.registerTool('get_queue_details', {
|
|
202
177
|
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',
|
|
178
|
+
inputSchema: z.object({}),
|
|
179
|
+
}, logged('get_queue_details', async () => {
|
|
180
|
+
const queues = getQueueNodes(currentGraph);
|
|
181
|
+
const queueFindings = currentFindings.filter((f) => f.metadata?.queueName);
|
|
182
|
+
return toText({
|
|
183
|
+
total: queues.length,
|
|
184
|
+
queues: queues.map((q) => ({
|
|
185
|
+
name: q.name, provider: q.provider, hasDLQ: q.hasDLQ, encrypted: q.encrypted,
|
|
186
|
+
approximateMessages: q.approximateMessages, retentionDays: q.retentionDays,
|
|
187
|
+
findings: queueFindings.filter((f) => f.metadata.queueName === q.name).map((f) => ({ severity: f.severity, issue: f.issue })),
|
|
188
|
+
})),
|
|
189
|
+
});
|
|
190
|
+
}));
|
|
191
|
+
mcp.registerTool('get_topic_details', {
|
|
219
192
|
description: 'Returns all SNS topics with subscription counts and protocols.',
|
|
220
|
-
inputSchema:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{
|
|
227
|
-
name: 'get_secrets_overview',
|
|
193
|
+
inputSchema: z.object({}),
|
|
194
|
+
}, logged('get_topic_details', async () => {
|
|
195
|
+
const topics = getTopicNodes(currentGraph);
|
|
196
|
+
return toText({ total: topics.length, topics: topics.map((t) => ({ name: t.name, provider: t.provider, subscriptionCount: t.subscriptionCount, encrypted: t.encrypted })) });
|
|
197
|
+
}));
|
|
198
|
+
mcp.registerTool('get_secrets_overview', {
|
|
228
199
|
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',
|
|
200
|
+
inputSchema: z.object({}),
|
|
201
|
+
}, logged('get_secrets_overview', async () => {
|
|
202
|
+
const secrets = getSecretNodes(currentGraph);
|
|
203
|
+
const secretFindings = currentFindings.filter((f) => f.metadata?.secretName);
|
|
204
|
+
return toText({
|
|
205
|
+
total: secrets.length, note: 'Secret values are never included in this response.',
|
|
206
|
+
secrets: secrets.map((s) => ({
|
|
207
|
+
name: s.name, provider: s.provider, rotationEnabled: s.rotationEnabled, rotationDays: s.rotationDays,
|
|
208
|
+
findings: secretFindings.filter((f) => f.metadata.secretName === s.name).map((f) => ({ severity: f.severity, issue: f.issue })),
|
|
209
|
+
})),
|
|
210
|
+
});
|
|
211
|
+
}));
|
|
212
|
+
mcp.registerTool('get_parameter_overview', {
|
|
244
213
|
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',
|
|
214
|
+
inputSchema: z.object({}),
|
|
215
|
+
}, logged('get_parameter_overview', async () => {
|
|
216
|
+
const parameters = getParameterNodes(currentGraph);
|
|
217
|
+
return toText({
|
|
218
|
+
total: parameters.length, note: 'Parameter values are never included in this response.',
|
|
219
|
+
parameters: parameters.map((p) => ({ name: p.name, provider: p.provider, type: p.paramType, tier: p.tier })),
|
|
220
|
+
});
|
|
221
|
+
}));
|
|
222
|
+
mcp.registerTool('get_lambda_overview', {
|
|
256
223
|
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',
|
|
224
|
+
inputSchema: z.object({}),
|
|
225
|
+
}, logged('get_lambda_overview', async () => {
|
|
226
|
+
const lambdas = getLambdaNodes(currentGraph);
|
|
227
|
+
const lambdaFindings = currentFindings.filter((f) => f.metadata?.functionName);
|
|
228
|
+
return toText({
|
|
229
|
+
total: lambdas.length, note: 'Environment variable values are never included.',
|
|
230
|
+
lambdas: lambdas.map((l) => ({
|
|
231
|
+
name: l.name, runtime: l.runtime, memoryMB: l.memoryMB, timeoutSec: l.timeoutSec,
|
|
232
|
+
envVarCount: l.envVarKeys?.length ?? 0, envVarKeys: l.envVarKeys,
|
|
233
|
+
findings: lambdaFindings.filter((f) => f.metadata.functionName === l.name).map((f) => ({ severity: f.severity, issue: f.issue })),
|
|
234
|
+
})),
|
|
235
|
+
});
|
|
236
|
+
}));
|
|
237
|
+
mcp.registerTool('get_log_errors', {
|
|
273
238
|
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}`);
|
|
239
|
+
inputSchema: z.object({ logGroup: z.string().describe('Filter to a specific log group name (optional)').optional() }),
|
|
240
|
+
}, logged('get_log_errors', async ({ logGroup: filterName }) => {
|
|
241
|
+
const logGroups = getLogGroupNodes(currentGraph).filter((lg) => !filterName || lg.name.includes(filterName));
|
|
242
|
+
return toText({
|
|
243
|
+
note: 'Only error patterns and counts are returned — no raw log messages.',
|
|
244
|
+
windowHours: 24,
|
|
245
|
+
logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never-expires', errorCount: lg.errorCount, topErrorPatterns: lg.topErrorPatterns })),
|
|
246
|
+
});
|
|
247
|
+
}));
|
|
248
|
+
return mcp;
|
|
315
249
|
}
|
|
316
250
|
// ── Fastify server ────────────────────────────────────────────────────────────
|
|
317
|
-
function createServer(port = 3000) {
|
|
318
|
-
const fastify = (
|
|
319
|
-
fastify.register(
|
|
251
|
+
export function createServer(port = 3000) {
|
|
252
|
+
const fastify = Fastify({ logger: false });
|
|
253
|
+
fastify.register(cors, { origin: true });
|
|
254
|
+
const mcp = createMcpServer();
|
|
320
255
|
fastify.get('/health', async () => ({
|
|
321
|
-
status: 'ok', version
|
|
256
|
+
status: 'ok', version,
|
|
322
257
|
graphNodes: currentGraph.nodes.length,
|
|
323
258
|
graphEdges: currentGraph.edges.length,
|
|
324
259
|
findings: currentFindings.length,
|
|
325
260
|
}));
|
|
326
|
-
fastify.get('/mcp/tools', async () => ({
|
|
327
|
-
tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
328
|
-
}));
|
|
329
261
|
fastify.post('/mcp', async (request, reply) => {
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
262
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
263
|
+
reply.raw.on('close', () => transport.close());
|
|
264
|
+
await mcp.connect(transport);
|
|
265
|
+
await transport.handleRequest(request.raw, reply.raw, request.body);
|
|
266
|
+
return reply;
|
|
334
267
|
});
|
|
335
268
|
return {
|
|
336
269
|
fastify,
|
|
337
270
|
start: async () => {
|
|
338
271
|
try {
|
|
339
272
|
await fastify.listen({ port, host: '0.0.0.0' });
|
|
340
|
-
|
|
273
|
+
logger.info(`Infrawise MCP server running at http://localhost:${port}`);
|
|
341
274
|
}
|
|
342
275
|
catch (e) {
|
|
343
|
-
|
|
276
|
+
logger.error(`Failed to start server: ${e instanceof Error ? e.message : String(e)}`);
|
|
344
277
|
process.exit(1);
|
|
345
278
|
}
|
|
346
279
|
},
|
|
347
280
|
};
|
|
348
281
|
}
|
|
282
|
+
export { currentGraph, currentFindings };
|
package/dist/types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|