infrawise 0.12.5 → 0.12.6
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 +1 -1
- package/dist/cli/commands/serve.js +61 -48
- package/dist/cli/commands/stdio.js +55 -46
- package/dist/server/index.js +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ Add to your editor's MCP config:
|
|
|
149
149
|
|
|
150
150
|
| Tool | What it provides |
|
|
151
151
|
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
152
|
-
| `get_infra_overview` | Complete snapshot — all services, counts,
|
|
152
|
+
| `get_infra_overview` | Complete snapshot — all services, counts, high-severity findings, and a `configured` flag |
|
|
153
153
|
| `get_graph_summary` | Full infrastructure graph — all nodes, edges, and findings |
|
|
154
154
|
| `analyze_function` | Issues in a specific function — scans, missing indexes, N+1, trigger event shapes |
|
|
155
155
|
| `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
|
|
@@ -2,8 +2,8 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import ora from 'ora';
|
|
5
|
-
import { loadConfig,
|
|
6
|
-
import { createServer, setGraphState } from '../../server/index.js';
|
|
5
|
+
import { loadConfig, readCache, setCacheDir } from '../../core/index.js';
|
|
6
|
+
import { createServer, setGraphState, setConfigured } from '../../server/index.js';
|
|
7
7
|
import { log, printHeader } from '../utils.js';
|
|
8
8
|
import { runAnalyze, runCodeRefresh } from './analyze.js';
|
|
9
9
|
import { runStdio } from './stdio.js';
|
|
@@ -26,8 +26,10 @@ const TOOL_MAP = [
|
|
|
26
26
|
{ name: 'get_api_routes', service: 'apiGateway' },
|
|
27
27
|
{ name: 'get_log_errors', service: 'cloudwatchLogs' },
|
|
28
28
|
];
|
|
29
|
+
// With no config every registered tool is shown as active (the server exposes
|
|
30
|
+
// all of them); a config narrows the list to its enabled services.
|
|
29
31
|
function isEnabled(cfg, service) {
|
|
30
|
-
if (!service)
|
|
32
|
+
if (!service || !cfg)
|
|
31
33
|
return true;
|
|
32
34
|
const svc = cfg[service];
|
|
33
35
|
return svc?.enabled === true;
|
|
@@ -63,16 +65,18 @@ export async function runServe(options = {}) {
|
|
|
63
65
|
}
|
|
64
66
|
const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : 3000);
|
|
65
67
|
printHeader('MCP Server');
|
|
68
|
+
setCacheDir(path.dirname(path.resolve(options.config ?? 'infrawise.yaml')));
|
|
69
|
+
// A hosted MCP runtime may launch the server with no infrawise.yaml. Start
|
|
70
|
+
// anyway with an empty graph so the host can connect and list tools.
|
|
66
71
|
let config;
|
|
67
72
|
try {
|
|
68
73
|
config = loadConfig(options.config);
|
|
69
|
-
setCacheDir(path.dirname(path.resolve(options.config ?? 'infrawise.yaml')));
|
|
70
74
|
log.success('Config loaded', options.config ?? 'infrawise.yaml');
|
|
71
75
|
}
|
|
72
76
|
catch (err) {
|
|
73
|
-
|
|
74
|
-
process.exit(1);
|
|
77
|
+
log.warn(`Starting with empty graph (no config loaded): ${err instanceof Error ? err.message : String(err)}`);
|
|
75
78
|
}
|
|
79
|
+
setConfigured(config !== undefined);
|
|
76
80
|
const repoPath = process.cwd();
|
|
77
81
|
// Auto-analyze if no cache
|
|
78
82
|
const cachedGraph = readCache('graph');
|
|
@@ -81,7 +85,7 @@ export async function runServe(options = {}) {
|
|
|
81
85
|
log.success('Cached analysis loaded', `${cachedGraph.nodes.length} nodes · ${cachedGraph.edges.length} edges · ${cachedFindings.length} finding(s)`);
|
|
82
86
|
setGraphState(cachedGraph, cachedFindings);
|
|
83
87
|
}
|
|
84
|
-
else {
|
|
88
|
+
else if (config) {
|
|
85
89
|
log.warn('No cache found — running analysis now...');
|
|
86
90
|
console.log('');
|
|
87
91
|
await runAnalyze({ repo: repoPath, config: options.config });
|
|
@@ -89,6 +93,9 @@ export async function runServe(options = {}) {
|
|
|
89
93
|
const freshFindings = readCache('findings') ?? [];
|
|
90
94
|
setGraphState(freshGraph, freshFindings);
|
|
91
95
|
}
|
|
96
|
+
else {
|
|
97
|
+
setGraphState({ nodes: [], edges: [] }, []);
|
|
98
|
+
}
|
|
92
99
|
console.log('');
|
|
93
100
|
// Start server
|
|
94
101
|
const spin = ora({ text: chalk.dim('Starting server...'), color: 'cyan' }).start();
|
|
@@ -127,49 +134,55 @@ export async function runServe(options = {}) {
|
|
|
127
134
|
console.log(chalk.dim(` claude mcp add --transport http infrawise ${mcpUrl}`));
|
|
128
135
|
console.log('');
|
|
129
136
|
console.log(chalk.dim(' Watching for file changes... Press Ctrl+C to stop\n'));
|
|
130
|
-
// File watch — re-run code analysis on save
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const ext = path.extname(filename);
|
|
144
|
-
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
|
|
145
|
-
return;
|
|
146
|
-
if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
|
|
147
|
-
return;
|
|
148
|
-
if (debounceTimer)
|
|
149
|
-
clearTimeout(debounceTimer);
|
|
150
|
-
debounceTimer = setTimeout(async () => {
|
|
151
|
-
if (refreshing)
|
|
137
|
+
// File watch — re-run code analysis on save (needs a config to drive analyzers)
|
|
138
|
+
if (config) {
|
|
139
|
+
const cfg = config;
|
|
140
|
+
let debounceTimer = null;
|
|
141
|
+
let refreshing = false;
|
|
142
|
+
const configFile = path.resolve(options.config ?? 'infrawise.yaml');
|
|
143
|
+
try {
|
|
144
|
+
fs.watch(repoPath, { recursive: true }, (_, filename) => {
|
|
145
|
+
if (!filename)
|
|
146
|
+
return;
|
|
147
|
+
const abs = path.join(repoPath, filename);
|
|
148
|
+
if (abs === configFile) {
|
|
149
|
+
console.log(chalk.dim('\n infrawise.yaml changed — restart to apply config changes\n'));
|
|
152
150
|
return;
|
|
153
|
-
refreshing = true;
|
|
154
|
-
const spin = ora({ text: chalk.dim('Refreshing code analysis...'), color: 'cyan' }).start();
|
|
155
|
-
try {
|
|
156
|
-
const { graph, findings } = await runCodeRefresh(repoPath, config);
|
|
157
|
-
setGraphState(graph, findings);
|
|
158
|
-
spin.succeed(chalk.green('Analysis refreshed') +
|
|
159
|
-
chalk.dim(` ${graph.nodes.length} nodes · ${findings.length} finding(s)`));
|
|
160
|
-
}
|
|
161
|
-
catch (err) {
|
|
162
|
-
spin.warn(chalk.yellow('Refresh failed') +
|
|
163
|
-
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
164
|
-
}
|
|
165
|
-
finally {
|
|
166
|
-
refreshing = false;
|
|
167
151
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
152
|
+
const ext = path.extname(filename);
|
|
153
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
|
|
154
|
+
return;
|
|
155
|
+
if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
|
|
156
|
+
return;
|
|
157
|
+
if (debounceTimer)
|
|
158
|
+
clearTimeout(debounceTimer);
|
|
159
|
+
debounceTimer = setTimeout(async () => {
|
|
160
|
+
if (refreshing)
|
|
161
|
+
return;
|
|
162
|
+
refreshing = true;
|
|
163
|
+
const spin = ora({
|
|
164
|
+
text: chalk.dim('Refreshing code analysis...'),
|
|
165
|
+
color: 'cyan',
|
|
166
|
+
}).start();
|
|
167
|
+
try {
|
|
168
|
+
const { graph, findings } = await runCodeRefresh(repoPath, cfg);
|
|
169
|
+
setGraphState(graph, findings);
|
|
170
|
+
spin.succeed(chalk.green('Analysis refreshed') +
|
|
171
|
+
chalk.dim(` ${graph.nodes.length} nodes · ${findings.length} finding(s)`));
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
spin.warn(chalk.yellow('Refresh failed') +
|
|
175
|
+
chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
refreshing = false;
|
|
179
|
+
}
|
|
180
|
+
}, 2000);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// fs.watch may not support recursive on all platforms — silently skip
|
|
185
|
+
}
|
|
173
186
|
}
|
|
174
187
|
process.on('SIGINT', () => {
|
|
175
188
|
console.log(chalk.dim('\n Shutting down...\n'));
|
|
@@ -1,72 +1,81 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { loadConfig,
|
|
5
|
-
import { createMcpServer, setGraphState } from '../../server/index.js';
|
|
4
|
+
import { loadConfig, readCache, setCacheDir } from '../../core/index.js';
|
|
5
|
+
import { createMcpServer, setGraphState, setConfigured } from '../../server/index.js';
|
|
6
6
|
import { runAnalyze, runCodeRefresh } from './analyze.js';
|
|
7
7
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
8
|
export async function runStdio(configPath) {
|
|
9
|
+
const resolvedConfigPath = configPath ?? 'infrawise.yaml';
|
|
10
|
+
setCacheDir(path.dirname(path.resolve(resolvedConfigPath)));
|
|
11
|
+
// A hosted MCP runtime (e.g. Glama) launches the server with no infrawise.yaml.
|
|
12
|
+
// Start anyway with an empty graph so the host can connect and list tools;
|
|
13
|
+
// analysis populates once a config is present.
|
|
9
14
|
let config;
|
|
10
15
|
try {
|
|
11
16
|
config = loadConfig(configPath);
|
|
12
17
|
}
|
|
13
18
|
catch (err) {
|
|
14
|
-
process.stderr.write(
|
|
15
|
-
process.exit(1);
|
|
19
|
+
process.stderr.write(`infrawise: starting with empty graph (no config loaded: ${err instanceof Error ? err.message : String(err)})\n`);
|
|
16
20
|
}
|
|
17
|
-
|
|
18
|
-
setCacheDir(path.dirname(path.resolve(resolvedConfigPath)));
|
|
21
|
+
setConfigured(config !== undefined);
|
|
19
22
|
const cachedGraph = readCache('graph', CACHE_TTL_MS);
|
|
20
23
|
const cachedFindings = readCache('findings', CACHE_TTL_MS);
|
|
21
24
|
if (cachedGraph && cachedFindings) {
|
|
22
25
|
setGraphState(cachedGraph, cachedFindings);
|
|
23
26
|
}
|
|
24
|
-
else {
|
|
27
|
+
else if (config) {
|
|
25
28
|
await runAnalyze({ config: configPath });
|
|
26
29
|
const graph = readCache('graph', CACHE_TTL_MS) ?? { nodes: [], edges: [] };
|
|
27
30
|
const findings = readCache('findings', CACHE_TTL_MS) ?? [];
|
|
28
31
|
setGraphState(graph, findings);
|
|
29
32
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const repoPath = process.cwd();
|
|
33
|
-
const configFile = path.resolve(resolvedConfigPath);
|
|
34
|
-
let debounceTimer = null;
|
|
35
|
-
let refreshing = false;
|
|
36
|
-
try {
|
|
37
|
-
fs.watch(repoPath, { recursive: true }, (_, filename) => {
|
|
38
|
-
if (!filename)
|
|
39
|
-
return;
|
|
40
|
-
const abs = path.join(repoPath, filename);
|
|
41
|
-
if (abs === configFile)
|
|
42
|
-
return;
|
|
43
|
-
const ext = path.extname(filename);
|
|
44
|
-
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
|
|
45
|
-
return;
|
|
46
|
-
if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
|
|
47
|
-
return;
|
|
48
|
-
if (debounceTimer)
|
|
49
|
-
clearTimeout(debounceTimer);
|
|
50
|
-
debounceTimer = setTimeout(async () => {
|
|
51
|
-
if (refreshing)
|
|
52
|
-
return;
|
|
53
|
-
refreshing = true;
|
|
54
|
-
try {
|
|
55
|
-
const { graph, findings } = await runCodeRefresh(repoPath, config);
|
|
56
|
-
setGraphState(graph, findings);
|
|
57
|
-
process.stderr.write(`infrawise: code graph refreshed (${graph.nodes.length} nodes · ${findings.length} finding(s))\n`);
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
// don't break the MCP connection on watcher errors
|
|
61
|
-
}
|
|
62
|
-
finally {
|
|
63
|
-
refreshing = false;
|
|
64
|
-
}
|
|
65
|
-
}, 2000);
|
|
66
|
-
});
|
|
33
|
+
else {
|
|
34
|
+
setGraphState({ nodes: [], edges: [] }, []);
|
|
67
35
|
}
|
|
68
|
-
|
|
69
|
-
|
|
36
|
+
// File watching drives runCodeRefresh, which needs a config — skip it without one.
|
|
37
|
+
// stderr is safe in stdio transport; stdout is reserved for MCP JSON-RPC.
|
|
38
|
+
if (config) {
|
|
39
|
+
const cfg = config;
|
|
40
|
+
const repoPath = process.cwd();
|
|
41
|
+
const configFile = path.resolve(resolvedConfigPath);
|
|
42
|
+
let debounceTimer = null;
|
|
43
|
+
let refreshing = false;
|
|
44
|
+
try {
|
|
45
|
+
fs.watch(repoPath, { recursive: true }, (_, filename) => {
|
|
46
|
+
if (!filename)
|
|
47
|
+
return;
|
|
48
|
+
const abs = path.join(repoPath, filename);
|
|
49
|
+
if (abs === configFile)
|
|
50
|
+
return;
|
|
51
|
+
const ext = path.extname(filename);
|
|
52
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
|
|
53
|
+
return;
|
|
54
|
+
if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
|
|
55
|
+
return;
|
|
56
|
+
if (debounceTimer)
|
|
57
|
+
clearTimeout(debounceTimer);
|
|
58
|
+
debounceTimer = setTimeout(async () => {
|
|
59
|
+
if (refreshing)
|
|
60
|
+
return;
|
|
61
|
+
refreshing = true;
|
|
62
|
+
try {
|
|
63
|
+
const { graph, findings } = await runCodeRefresh(repoPath, cfg);
|
|
64
|
+
setGraphState(graph, findings);
|
|
65
|
+
process.stderr.write(`infrawise: code graph refreshed (${graph.nodes.length} nodes · ${findings.length} finding(s))\n`);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// don't break the MCP connection on watcher errors
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
refreshing = false;
|
|
72
|
+
}
|
|
73
|
+
}, 2000);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// fs.watch recursive not supported on all platforms — silently skip
|
|
78
|
+
}
|
|
70
79
|
}
|
|
71
80
|
const mcp = createMcpServer();
|
|
72
81
|
const transport = new StdioServerTransport();
|
package/dist/server/index.js
CHANGED
|
@@ -12,10 +12,17 @@ import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecre
|
|
|
12
12
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
13
13
|
let currentGraph = { nodes: [], edges: [] };
|
|
14
14
|
let currentFindings = [];
|
|
15
|
+
// False when the server booted without an infrawise.yaml (e.g. a hosted MCP
|
|
16
|
+
// runtime). Used to return a "run locally" hint instead of a bare empty graph.
|
|
17
|
+
let configured = true;
|
|
15
18
|
export function setGraphState(graph, findings) {
|
|
16
19
|
currentGraph = graph;
|
|
17
20
|
currentFindings = findings;
|
|
18
21
|
}
|
|
22
|
+
export function setConfigured(value) {
|
|
23
|
+
configured = value;
|
|
24
|
+
}
|
|
25
|
+
const NOT_CONFIGURED_HINT = 'No infrastructure loaded. infrawise reads your live infra locally — run `npx infrawise start` in your project (with AWS credentials and an infrawise.yaml) so these tools return your real DynamoDB/RDS/SQS/Lambda/etc. context. A remotely hosted instance has no access to your cloud account or code, so it returns empty results by design.';
|
|
19
26
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
20
27
|
function toText(data) {
|
|
21
28
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
@@ -31,7 +38,7 @@ function logged(name, fn) {
|
|
|
31
38
|
export function createMcpServer() {
|
|
32
39
|
const mcp = new McpServer({ name: 'infrawise', version });
|
|
33
40
|
mcp.registerTool('get_infra_overview', {
|
|
34
|
-
description: 'Returns a compact infrastructure snapshot: service counts, all databases, queues, topics, secrets, lambdas, and high-severity findings. Call this first at the start of any database or infrastructure task to understand what services are in scope. Prefer this over get_graph_summary for quick orientation; use get_graph_summary only when you need every node, edge, and finding in full.',
|
|
41
|
+
description: 'Returns a compact infrastructure snapshot: service counts, all databases, queues, topics, secrets, lambdas, and high-severity findings. Call this first at the start of any database or infrastructure task to understand what services are in scope. Prefer this over get_graph_summary for quick orientation; use get_graph_summary only when you need every node, edge, and finding in full. Also returns a `configured` flag — when false, the server has no infrawise.yaml loaded (e.g. a remotely hosted instance) and all tools return empty results; a `setupHint` explains how to run infrawise locally.',
|
|
35
42
|
inputSchema: z.object({}),
|
|
36
43
|
}, logged('get_infra_overview', async () => {
|
|
37
44
|
const tables = getTableNodes(currentGraph);
|
|
@@ -44,6 +51,8 @@ export function createMcpServer() {
|
|
|
44
51
|
const functions = getFunctionNodes(currentGraph);
|
|
45
52
|
const buckets = getBucketNodes(currentGraph);
|
|
46
53
|
return toText({
|
|
54
|
+
configured,
|
|
55
|
+
...(configured ? {} : { setupHint: NOT_CONFIGURED_HINT }),
|
|
47
56
|
summary: {
|
|
48
57
|
tables: tables.length,
|
|
49
58
|
functions: functions.length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.6",
|
|
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, API Gateway, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
|
|
6
6
|
"keywords": [
|