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.
@@ -1,348 +1,282 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.currentFindings = exports.currentGraph = void 0;
7
- exports.setGraphState = setGraphState;
8
- exports.createServer = createServer;
9
- const fastify_1 = __importDefault(require("fastify"));
10
- const cors_1 = __importDefault(require("@fastify/cors"));
11
- const core_1 = require("../core");
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
- exports.currentFindings = currentFindings;
19
- function setGraphState(graph, findings) {
20
- exports.currentGraph = currentGraph = graph;
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
- // ── Tools ────────────────────────────────────────────────────────────────────
27
- const TOOLS = [
28
- {
29
- name: 'get_infra_overview',
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: { type: 'object', properties: {} },
32
- handler: async () => {
33
- const tables = (0, graph_1.getTableNodes)(currentGraph);
34
- const queues = (0, graph_1.getQueueNodes)(currentGraph);
35
- const topics = (0, graph_1.getTopicNodes)(currentGraph);
36
- const secrets = (0, graph_1.getSecretNodes)(currentGraph);
37
- const parameters = (0, graph_1.getParameterNodes)(currentGraph);
38
- const logGroups = (0, graph_1.getLogGroupNodes)(currentGraph);
39
- const lambdas = (0, graph_1.getLambdaNodes)(currentGraph);
40
- const functions = (0, graph_1.getFunctionNodes)(currentGraph);
41
- return toText({
42
- summary: {
43
- tables: tables.length, functions: functions.length,
44
- queues: queues.length, topics: topics.length,
45
- secrets: secrets.length, parameters: parameters.length,
46
- logGroups: logGroups.length, lambdas: lambdas.length,
47
- totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
48
- findings: (0, analyzers_1.summarizeFindings)(currentFindings),
49
- },
50
- databases: tables.map((t) => ({ name: t.name, type: t.databaseType })),
51
- queues: queues.map((q) => ({ name: q.name, hasDLQ: q.hasDLQ, encrypted: q.encrypted, approximateMessages: q.approximateMessages })),
52
- topics: topics.map((t) => ({ name: t.name, subscriptions: t.subscriptionCount })),
53
- secrets: secrets.map((s) => ({ name: s.name, rotationEnabled: s.rotationEnabled })),
54
- parameters: parameters.map((p) => ({ name: p.name, type: p.paramType, tier: p.tier })),
55
- lambdas: lambdas.map((l) => ({ name: l.name, runtime: l.runtime, memoryMB: l.memoryMB })),
56
- logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never', errorCount: lg.errorCount })),
57
- highFindings: currentFindings.filter((f) => f.severity === 'high').map((f) => ({ issue: f.issue, recommendation: f.recommendation })),
58
- });
59
- },
60
- },
61
- {
62
- name: 'get_graph_summary',
63
- description: 'Returns the full infrastructure graph (all nodes and edges) plus findings summary.',
64
- inputSchema: { type: 'object', properties: {} },
65
- handler: async () => toText({
66
- nodes: currentGraph.nodes,
67
- edges: currentGraph.edges,
68
- findings: currentFindings,
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
- tables: (0, graph_1.getTableNodes)(currentGraph).length, functions: (0, graph_1.getFunctionNodes)(currentGraph).length,
72
- queues: (0, graph_1.getQueueNodes)(currentGraph).length, scans: (0, graph_1.getScanEdges)(currentGraph).length,
73
- ...(0, analyzers_1.summarizeFindings)(currentFindings),
52
+ findings: summarizeFindings(currentFindings),
74
53
  },
75
- }),
76
- },
77
- {
78
- name: 'analyze_function',
79
- description: 'Analyze a specific function for all infrastructure issues: DB queries, queue publishing, secret access, etc.',
80
- inputSchema: {
81
- type: 'object',
82
- properties: { function: { type: 'string', description: 'Function name to analyze' } },
83
- required: ['function'],
84
- },
85
- handler: async ({ function: functionName }) => {
86
- const funcNode = currentGraph.nodes.find((n) => n.type === 'function' && n.name === functionName);
87
- if (!funcNode) {
88
- return toText({ function: functionName, found: false, issues: [], recommendations: [`Function "${String(functionName)}" not found in the analyzed codebase.`] });
89
- }
90
- const outEdges = (0, graph_1.getOutgoingEdges)(currentGraph, funcNode.id);
91
- const relatedFindings = currentFindings.filter((f) => {
92
- const meta = f.metadata;
93
- return meta?.functionName === functionName || String(meta?.callerFunctions ?? '').includes(String(functionName));
94
- });
95
- return toText({
96
- function: functionName, found: true,
97
- file: funcNode.type === 'function' ? funcNode.file : undefined,
98
- accesses: outEdges.map((e) => {
99
- const target = currentGraph.nodes.find((n) => n.id === e.to);
100
- return { targetId: e.to, edgeType: e.type, targetName: target && 'name' in target ? target.name : e.to, targetType: target?.type };
101
- }),
102
- issues: relatedFindings.map((f) => ({ severity: f.severity, issue: f.issue, description: f.description })),
103
- recommendations: [...new Set(relatedFindings.map((f) => f.recommendation))],
104
- });
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
- name: 'suggest_gsi',
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
- type: 'object',
112
- properties: {
113
- table: { type: 'string', description: 'DynamoDB table name' },
114
- attribute: { type: 'string', description: 'Attribute to create the GSI on' },
115
- },
116
- required: ['table', 'attribute'],
117
- },
118
- handler: async ({ table: tableName, attribute }) => {
119
- const sanitizedAttr = String(attribute).replace(/[^a-zA-Z0-9_]/g, '_');
120
- const indexName = `${String(tableName)}-${sanitizedAttr}-index`;
121
- const tableNode = currentGraph.nodes.find((n) => n.type === 'table' && n.databaseType === 'dynamodb' && 'name' in n && n.name === tableName);
122
- return toText({
123
- table: tableName, attribute, found: !!tableNode,
124
- index: { name: indexName, partitionKey: attribute, projectionType: 'ALL', billingMode: 'PAY_PER_REQUEST' },
125
- rationale: `A GSI on "${String(attribute)}" allows Query instead of Scan when filtering by this attribute.`,
126
- recommendation: `Add GSI "${indexName}" with partition key "${String(attribute)}" to your IaC definition.`,
127
- });
128
- },
129
- },
130
- {
131
- name: 'postgres_index_suggestions',
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
- type: 'object',
135
- properties: {
136
- table: { type: 'string', description: 'PostgreSQL table name' },
137
- column: { type: 'string', description: 'Column name to index' },
138
- },
139
- required: ['table', 'column'],
140
- },
141
- handler: async ({ table: tableName, column }) => {
142
- const sanitizedCol = String(column).replace(/[^a-zA-Z0-9_]/g, '_');
143
- const sanitizedTable = String(tableName).replace(/[^a-zA-Z0-9_]/g, '_');
144
- const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
145
- return toText({
146
- table: tableName, column,
147
- recommendation: `CREATE INDEX CONCURRENTLY ${indexName} ON ${String(tableName)} (${String(column)});`,
148
- rationale: `An index on "${String(column)}" eliminates sequential scans when filtering on this column.`,
149
- notes: ['Use CONCURRENTLY to avoid locking the table', 'Run ANALYZE after creation',
150
- `Partial index: CREATE INDEX CONCURRENTLY ${indexName}_partial ON ${String(tableName)} (${String(column)}) WHERE ${String(column)} IS NOT NULL;`],
151
- });
152
- },
153
- },
154
- {
155
- name: 'suggest_mongo_index',
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
- type: 'object',
159
- properties: {
160
- collection: { type: 'string', description: 'MongoDB collection name' },
161
- field: { type: 'string', description: 'Field name to index' },
162
- },
163
- required: ['collection', 'field'],
164
- },
165
- handler: async ({ collection, field }) => toText({
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.${String(collection)}.createIndex({ ${String(field)}: 1 })`,
168
- rationale: `An index on "${String(field)}" eliminates full collection scans when filtering on this field.`,
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.${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 })`,
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
- type: 'object',
181
- properties: {
182
- table: { type: 'string', description: 'MySQL table name' },
183
- column: { type: 'string', description: 'Column name to index' },
184
- },
185
- required: ['table', 'column'],
186
- },
187
- handler: async ({ table: tableName, column }) => {
188
- const sanitizedCol = String(column).replace(/[^a-zA-Z0-9_]/g, '_');
189
- const sanitizedTable = String(tableName).replace(/[^a-zA-Z0-9_]/g, '_');
190
- const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
191
- return toText({
192
- table: tableName, column,
193
- recommendation: `ALTER TABLE ${String(tableName)} ADD INDEX ${indexName} (${String(column)});`,
194
- rationale: `An index on "${String(column)}" eliminates full table scans when filtering on this column.`,
195
- notes: ['MySQL InnoDB adds indexes online (no full lock for 5.6+)', 'EXPLAIN SELECT ... to verify after adding',
196
- `Composite: ALTER TABLE ${String(tableName)} ADD INDEX idx_composite (${String(column)}, other_column);`],
197
- });
198
- },
199
- },
200
- {
201
- name: 'get_queue_details',
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: { type: 'object', properties: {} },
204
- handler: async () => {
205
- const queues = (0, graph_1.getQueueNodes)(currentGraph);
206
- const queueFindings = currentFindings.filter((f) => f.metadata?.queueName);
207
- return toText({
208
- total: queues.length,
209
- queues: queues.map((q) => ({
210
- name: q.name, provider: q.provider, hasDLQ: q.hasDLQ, encrypted: q.encrypted,
211
- approximateMessages: q.approximateMessages, retentionDays: q.retentionDays,
212
- findings: queueFindings.filter((f) => f.metadata.queueName === q.name).map((f) => ({ severity: f.severity, issue: f.issue })),
213
- })),
214
- });
215
- },
216
- },
217
- {
218
- name: 'get_topic_details',
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: { type: 'object', properties: {} },
221
- handler: async () => {
222
- const topics = (0, graph_1.getTopicNodes)(currentGraph);
223
- return toText({ total: topics.length, topics: topics.map((t) => ({ name: t.name, provider: t.provider, subscriptionCount: t.subscriptionCount, encrypted: t.encrypted })) });
224
- },
225
- },
226
- {
227
- name: 'get_secrets_overview',
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: { type: 'object', properties: {} },
230
- handler: async () => {
231
- const secrets = (0, graph_1.getSecretNodes)(currentGraph);
232
- const secretFindings = currentFindings.filter((f) => f.metadata?.secretName);
233
- return toText({
234
- total: secrets.length, note: 'Secret values are never included in this response.',
235
- secrets: secrets.map((s) => ({
236
- name: s.name, provider: s.provider, rotationEnabled: s.rotationEnabled, rotationDays: s.rotationDays,
237
- findings: secretFindings.filter((f) => f.metadata.secretName === s.name).map((f) => ({ severity: f.severity, issue: f.issue })),
238
- })),
239
- });
240
- },
241
- },
242
- {
243
- name: 'get_parameter_overview',
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: { type: 'object', properties: {} },
246
- handler: async () => {
247
- const parameters = (0, graph_1.getParameterNodes)(currentGraph);
248
- return toText({
249
- total: parameters.length, note: 'Parameter values are never included in this response.',
250
- parameters: parameters.map((p) => ({ name: p.name, provider: p.provider, type: p.paramType, tier: p.tier })),
251
- });
252
- },
253
- },
254
- {
255
- name: 'get_lambda_overview',
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: { type: 'object', properties: {} },
258
- handler: async () => {
259
- const lambdas = (0, graph_1.getLambdaNodes)(currentGraph);
260
- const lambdaFindings = currentFindings.filter((f) => f.metadata?.functionName);
261
- return toText({
262
- total: lambdas.length, note: 'Environment variable values are never included.',
263
- lambdas: lambdas.map((l) => ({
264
- name: l.name, runtime: l.runtime, memoryMB: l.memoryMB, timeoutSec: l.timeoutSec,
265
- envVarCount: l.envVarKeys?.length ?? 0, envVarKeys: l.envVarKeys,
266
- findings: lambdaFindings.filter((f) => f.metadata.functionName === l.name).map((f) => ({ severity: f.severity, issue: f.issue })),
267
- })),
268
- });
269
- },
270
- },
271
- {
272
- name: 'get_log_errors',
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
- type: 'object',
276
- properties: { logGroup: { type: 'string', description: 'Filter to a specific log group name (optional)' } },
277
- },
278
- handler: async ({ logGroup: filterName }) => {
279
- const logGroups = (0, graph_1.getLogGroupNodes)(currentGraph).filter((lg) => !filterName || lg.name.includes(String(filterName)));
280
- return toText({
281
- note: 'Only error patterns and counts are returned — no raw log messages.',
282
- windowHours: 24,
283
- logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never-expires', errorCount: lg.errorCount, topErrorPatterns: lg.topErrorPatterns })),
284
- });
285
- },
286
- },
287
- ];
288
- const ok = (id, result) => ({ jsonrpc: '2.0', id, result });
289
- const rpcErr = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
290
- async function handleMcp(body) {
291
- const { method, params = {}, id } = body;
292
- if (method === 'initialize') {
293
- return ok(id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'infrawise', version: '0.1.0' } });
294
- }
295
- if (method === 'notifications/initialized' || method === 'ping') {
296
- return id != null ? ok(id, {}) : null;
297
- }
298
- if (method === 'tools/list') {
299
- return ok(id, { tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })) });
300
- }
301
- if (method === 'tools/call') {
302
- const { name, arguments: args = {} } = params;
303
- const tool = TOOLS.find((t) => t.name === name);
304
- if (!tool)
305
- return rpcErr(id, -32601, `Unknown tool: ${name}`);
306
- core_1.logger.info(`→ ${name}${Object.keys(args).length ? ` ${JSON.stringify(args)}` : ''}`);
307
- try {
308
- return ok(id, await tool.handler(args));
309
- }
310
- catch (e) {
311
- return rpcErr(id, -32603, e instanceof Error ? e.message : String(e));
312
- }
313
- }
314
- return rpcErr(id, -32601, `Method not found: ${method}`);
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 = (0, fastify_1.default)({ logger: false });
319
- fastify.register(cors_1.default, { origin: true });
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: '0.1.0',
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 response = await handleMcp(request.body);
331
- if (response === null)
332
- return reply.code(204).send();
333
- return response;
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
- core_1.logger.info(`Infrawise MCP server running at http://localhost:${port}`);
273
+ logger.info(`Infrawise MCP server running at http://localhost:${port}`);
341
274
  }
342
275
  catch (e) {
343
- core_1.logger.error(`Failed to start server: ${e instanceof Error ? e.message : String(e)}`);
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};