infrawise 0.14.0 → 0.15.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 +3 -2
- package/dist/adapters/db/mysql.js +37 -1
- package/dist/adapters/db/postgres.js +49 -2
- package/dist/cli/commands/serve.js +1 -0
- package/dist/graph/index.js +15 -1
- package/dist/server/index.js +51 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ That's it. Infrawise will:
|
|
|
74
74
|
1. Probe your environment and generate `infrawise.yaml` (first time only — asks which AWS profile to use only if you have several)
|
|
75
75
|
2. Scan your AWS services, databases, and codebase
|
|
76
76
|
3. Write `.mcp.json` so your editor auto-connects on every future launch
|
|
77
|
-
4. Open Claude Code with all
|
|
77
|
+
4. Open Claude Code with all 21 MCP tools ready
|
|
78
78
|
|
|
79
79
|
**Every time after:**
|
|
80
80
|
|
|
@@ -117,7 +117,7 @@ Writes `.mcp.json` to your project root and opens Claude Code. Claude Code reads
|
|
|
117
117
|
infrawise start --cursor
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
Writes `.cursor/mcp.json` and opens Cursor. All
|
|
120
|
+
Writes `.cursor/mcp.json` and opens Cursor. All 21 infrawise tools are available in Cursor's MCP panel.
|
|
121
121
|
|
|
122
122
|
### Any editor (no flag)
|
|
123
123
|
|
|
@@ -153,6 +153,7 @@ Add to your editor's MCP config:
|
|
|
153
153
|
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
154
154
|
| `get_infra_overview` | Complete snapshot — services, counts, high-severity findings, analysis `freshness` (age + stale flag), `configured` flag |
|
|
155
155
|
| `get_graph_summary` | Full infrastructure graph — all nodes, edges, and findings |
|
|
156
|
+
| `get_table_schema` | Column-level schema for named tables/collections — types, PKs, FKs, indexes, DynamoDB keys (no row data) |
|
|
156
157
|
| `analyze_function` | Issues in a specific function — scans, missing indexes, N+1, trigger event shapes, missing IAM permissions |
|
|
157
158
|
| `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
|
|
158
159
|
| `postgres_index_suggestions` | Exact `CREATE INDEX` SQL for your actual table |
|
|
@@ -28,10 +28,19 @@ export async function extractMySQLMetadata(connectionString) {
|
|
|
28
28
|
logger.debug(`Found ${tableRows.length} MySQL table(s)`);
|
|
29
29
|
// Get all columns
|
|
30
30
|
const [columnRows] = await connection.execute(`
|
|
31
|
-
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME
|
|
31
|
+
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE
|
|
32
32
|
FROM information_schema.columns
|
|
33
33
|
WHERE TABLE_SCHEMA NOT IN (${[...SYSTEM_SCHEMAS].map(() => '?').join(', ')})
|
|
34
34
|
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
|
|
35
|
+
`, [...SYSTEM_SCHEMAS]);
|
|
36
|
+
// Get foreign keys
|
|
37
|
+
const [fkRows] = await connection.execute(`
|
|
38
|
+
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME,
|
|
39
|
+
REFERENCED_TABLE_SCHEMA, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
|
|
40
|
+
FROM information_schema.key_column_usage
|
|
41
|
+
WHERE REFERENCED_TABLE_NAME IS NOT NULL
|
|
42
|
+
AND TABLE_SCHEMA NOT IN (${[...SYSTEM_SCHEMAS].map(() => '?').join(', ')})
|
|
43
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
|
|
35
44
|
`, [...SYSTEM_SCHEMAS]);
|
|
36
45
|
// Get all indexes
|
|
37
46
|
const [indexRows] = await connection.execute(`
|
|
@@ -51,6 +60,7 @@ export async function extractMySQLMetadata(connectionString) {
|
|
|
51
60
|
`, [...SYSTEM_SCHEMAS]);
|
|
52
61
|
// Build lookup maps
|
|
53
62
|
const columnMap = new Map();
|
|
63
|
+
const columnDetailMap = new Map();
|
|
54
64
|
for (const row of columnRows) {
|
|
55
65
|
const key = `${row['TABLE_SCHEMA']}.${row['TABLE_NAME']}`;
|
|
56
66
|
let cols = columnMap.get(key);
|
|
@@ -59,6 +69,30 @@ export async function extractMySQLMetadata(connectionString) {
|
|
|
59
69
|
columnMap.set(key, cols);
|
|
60
70
|
}
|
|
61
71
|
cols.push(row['COLUMN_NAME']);
|
|
72
|
+
let details = columnDetailMap.get(key);
|
|
73
|
+
if (!details) {
|
|
74
|
+
details = [];
|
|
75
|
+
columnDetailMap.set(key, details);
|
|
76
|
+
}
|
|
77
|
+
details.push({
|
|
78
|
+
name: row['COLUMN_NAME'],
|
|
79
|
+
dataType: row['DATA_TYPE'],
|
|
80
|
+
nullable: row['IS_NULLABLE'] === 'YES',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const fkMap = new Map();
|
|
84
|
+
for (const row of fkRows) {
|
|
85
|
+
const key = `${row['TABLE_SCHEMA']}.${row['TABLE_NAME']}`;
|
|
86
|
+
let fks = fkMap.get(key);
|
|
87
|
+
if (!fks) {
|
|
88
|
+
fks = [];
|
|
89
|
+
fkMap.set(key, fks);
|
|
90
|
+
}
|
|
91
|
+
fks.push({
|
|
92
|
+
column: row['COLUMN_NAME'],
|
|
93
|
+
referencesTable: `${row['REFERENCED_TABLE_SCHEMA']}.${row['REFERENCED_TABLE_NAME']}`,
|
|
94
|
+
referencesColumn: row['REFERENCED_COLUMN_NAME'],
|
|
95
|
+
});
|
|
62
96
|
}
|
|
63
97
|
const indexMap = new Map();
|
|
64
98
|
for (const row of indexRows) {
|
|
@@ -96,6 +130,8 @@ export async function extractMySQLMetadata(connectionString) {
|
|
|
96
130
|
indexes: indexMap.get(key) ?? [],
|
|
97
131
|
primaryKeys: pkMap.get(key) ?? [],
|
|
98
132
|
engine,
|
|
133
|
+
columnDetails: columnDetailMap.get(key) ?? [],
|
|
134
|
+
foreignKeys: fkMap.get(key) ?? [],
|
|
99
135
|
});
|
|
100
136
|
}
|
|
101
137
|
return results;
|
|
@@ -22,7 +22,7 @@ export async function extractPostgresMetadata(connectionString) {
|
|
|
22
22
|
logger.debug(`Found ${tablesResult.rows.length} PostgreSQL table(s)`);
|
|
23
23
|
// Get all columns
|
|
24
24
|
const columnsResult = await client.query(`
|
|
25
|
-
SELECT table_schema, table_name, column_name
|
|
25
|
+
SELECT table_schema, table_name, column_name, data_type, is_nullable
|
|
26
26
|
FROM information_schema.columns
|
|
27
27
|
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
28
28
|
ORDER BY table_schema, table_name, ordinal_position
|
|
@@ -48,8 +48,29 @@ export async function extractPostgresMetadata(connectionString) {
|
|
|
48
48
|
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
49
49
|
ORDER BY tc.table_schema, tc.table_name
|
|
50
50
|
`);
|
|
51
|
-
//
|
|
51
|
+
// Get foreign keys
|
|
52
|
+
const foreignKeysResult = await client.query(`
|
|
53
|
+
SELECT
|
|
54
|
+
tc.table_schema,
|
|
55
|
+
tc.table_name,
|
|
56
|
+
kcu.column_name,
|
|
57
|
+
ccu.table_schema AS ref_schema,
|
|
58
|
+
ccu.table_name AS ref_table,
|
|
59
|
+
ccu.column_name AS ref_column
|
|
60
|
+
FROM information_schema.table_constraints tc
|
|
61
|
+
JOIN information_schema.key_column_usage kcu
|
|
62
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
63
|
+
AND tc.table_schema = kcu.table_schema
|
|
64
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
65
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
66
|
+
AND tc.table_schema = ccu.table_schema
|
|
67
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
68
|
+
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
69
|
+
ORDER BY tc.table_schema, tc.table_name
|
|
70
|
+
`);
|
|
71
|
+
// Build maps for columns, indexes, primary keys, foreign keys
|
|
52
72
|
const columnMap = new Map();
|
|
73
|
+
const columnDetailMap = new Map();
|
|
53
74
|
for (const row of columnsResult.rows) {
|
|
54
75
|
const key = `${row.table_schema}.${row.table_name}`;
|
|
55
76
|
let cols = columnMap.get(key);
|
|
@@ -58,6 +79,30 @@ export async function extractPostgresMetadata(connectionString) {
|
|
|
58
79
|
columnMap.set(key, cols);
|
|
59
80
|
}
|
|
60
81
|
cols.push(row.column_name);
|
|
82
|
+
let details = columnDetailMap.get(key);
|
|
83
|
+
if (!details) {
|
|
84
|
+
details = [];
|
|
85
|
+
columnDetailMap.set(key, details);
|
|
86
|
+
}
|
|
87
|
+
details.push({
|
|
88
|
+
name: row.column_name,
|
|
89
|
+
dataType: row.data_type,
|
|
90
|
+
nullable: row.is_nullable === 'YES',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const fkMap = new Map();
|
|
94
|
+
for (const row of foreignKeysResult.rows) {
|
|
95
|
+
const key = `${row.table_schema}.${row.table_name}`;
|
|
96
|
+
let fks = fkMap.get(key);
|
|
97
|
+
if (!fks) {
|
|
98
|
+
fks = [];
|
|
99
|
+
fkMap.set(key, fks);
|
|
100
|
+
}
|
|
101
|
+
fks.push({
|
|
102
|
+
column: row.column_name,
|
|
103
|
+
referencesTable: `${row.ref_schema}.${row.ref_table}`,
|
|
104
|
+
referencesColumn: row.ref_column,
|
|
105
|
+
});
|
|
61
106
|
}
|
|
62
107
|
const indexMap = new Map();
|
|
63
108
|
for (const row of indexesResult.rows) {
|
|
@@ -89,6 +134,8 @@ export async function extractPostgresMetadata(connectionString) {
|
|
|
89
134
|
columns: columnMap.get(key) ?? [],
|
|
90
135
|
indexes: indexMap.get(key) ?? [],
|
|
91
136
|
primaryKeys: pkMap.get(key) ?? [],
|
|
137
|
+
columnDetails: columnDetailMap.get(key) ?? [],
|
|
138
|
+
foreignKeys: fkMap.get(key) ?? [],
|
|
92
139
|
});
|
|
93
140
|
}
|
|
94
141
|
return results;
|
|
@@ -11,6 +11,7 @@ const BOX_W = 52;
|
|
|
11
11
|
const TOOL_MAP = [
|
|
12
12
|
{ name: 'get_infra_overview' },
|
|
13
13
|
{ name: 'get_graph_summary' },
|
|
14
|
+
{ name: 'get_table_schema' },
|
|
14
15
|
{ name: 'analyze_function' },
|
|
15
16
|
{ name: 'suggest_gsi', service: 'dynamodb' },
|
|
16
17
|
{ name: 'postgres_index_suggestions', service: 'postgres' },
|
package/dist/graph/index.js
CHANGED
|
@@ -13,7 +13,14 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
13
13
|
// ── Database tables ──────────────────────────────────────────────────────
|
|
14
14
|
for (const table of dynamoMeta) {
|
|
15
15
|
const nodeId = `table:dynamo:${table.tableName}`;
|
|
16
|
-
addNode({
|
|
16
|
+
addNode({
|
|
17
|
+
id: nodeId,
|
|
18
|
+
type: 'table',
|
|
19
|
+
name: table.tableName,
|
|
20
|
+
databaseType: 'dynamodb',
|
|
21
|
+
partitionKey: table.partitionKey,
|
|
22
|
+
sortKey: table.sortKey,
|
|
23
|
+
});
|
|
17
24
|
for (const indexName of table.indexes) {
|
|
18
25
|
const indexNodeId = `index:${table.tableName}:${indexName}`;
|
|
19
26
|
addNode({ id: indexNodeId, type: 'index', name: indexName });
|
|
@@ -27,6 +34,9 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
27
34
|
type: 'table',
|
|
28
35
|
name: `${table.schema}.${table.table}`,
|
|
29
36
|
databaseType: 'postgres',
|
|
37
|
+
columns: table.columnDetails,
|
|
38
|
+
primaryKeys: table.primaryKeys,
|
|
39
|
+
foreignKeys: table.foreignKeys,
|
|
30
40
|
});
|
|
31
41
|
for (const indexName of table.indexes) {
|
|
32
42
|
const indexNodeId = `index:${table.schema}.${table.table}:${indexName}`;
|
|
@@ -41,6 +51,9 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
41
51
|
type: 'table',
|
|
42
52
|
name: `${table.schema}.${table.table}`,
|
|
43
53
|
databaseType: 'mysql',
|
|
54
|
+
columns: table.columnDetails,
|
|
55
|
+
primaryKeys: table.primaryKeys,
|
|
56
|
+
foreignKeys: table.foreignKeys,
|
|
44
57
|
});
|
|
45
58
|
for (const indexName of table.indexes) {
|
|
46
59
|
const indexNodeId = `index:${table.schema}.${table.table}:${indexName}`;
|
|
@@ -55,6 +68,7 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
55
68
|
type: 'table',
|
|
56
69
|
name: `${coll.database}.${coll.collection}`,
|
|
57
70
|
databaseType: 'mongodb',
|
|
71
|
+
estimatedCount: coll.estimatedCount,
|
|
58
72
|
});
|
|
59
73
|
for (const idx of coll.indexes) {
|
|
60
74
|
if (idx.name === '_id_')
|
package/dist/server/index.js
CHANGED
|
@@ -487,6 +487,56 @@ export function createMcpServer() {
|
|
|
487
487
|
})),
|
|
488
488
|
});
|
|
489
489
|
}));
|
|
490
|
+
mcp.registerTool('get_table_schema', {
|
|
491
|
+
description: 'Returns the full schema for specific tables or collections by name: columns with data types and nullability, primary keys, foreign keys (join paths), indexes, DynamoDB partition/sort keys, and MongoDB estimated document counts. Accepts short names ("orders" matches "public.orders") and is case-insensitive. Call this after get_infra_overview when you need column-level detail to write a SQL query, DynamoDB expression, or MongoDB filter for specific tables — instead of pulling every schema with get_graph_summary. Do NOT call for a table inventory; use get_infra_overview for that. Row data is never included.',
|
|
492
|
+
inputSchema: z.object({
|
|
493
|
+
tables: z
|
|
494
|
+
.array(z.string())
|
|
495
|
+
.min(1)
|
|
496
|
+
.max(20)
|
|
497
|
+
.describe('Table or collection names to fetch schemas for'),
|
|
498
|
+
}),
|
|
499
|
+
}, logged('get_table_schema', async ({ tables }) => {
|
|
500
|
+
const tableNodes = getTableNodes(currentGraph);
|
|
501
|
+
const indexNamesFor = (nodeId) => currentGraph.edges
|
|
502
|
+
.filter((e) => e.from === nodeId && e.type === 'uses_index')
|
|
503
|
+
.map((e) => currentGraph.nodes.find((n) => n.id === e.to))
|
|
504
|
+
.filter((n) => n?.type === 'index')
|
|
505
|
+
.map((n) => n.name);
|
|
506
|
+
const results = tables.map((requested) => {
|
|
507
|
+
const lower = requested.toLowerCase();
|
|
508
|
+
const matches = tableNodes.filter((t) => {
|
|
509
|
+
const name = t.name.toLowerCase();
|
|
510
|
+
return name === lower || name.split('.').pop() === lower;
|
|
511
|
+
});
|
|
512
|
+
if (matches.length === 0) {
|
|
513
|
+
const suggestions = tableNodes
|
|
514
|
+
.filter((t) => t.name.toLowerCase().includes(lower))
|
|
515
|
+
.map((t) => t.name)
|
|
516
|
+
.slice(0, 5);
|
|
517
|
+
return { requested, found: false, ...(suggestions.length ? { suggestions } : {}) };
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
requested,
|
|
521
|
+
found: true,
|
|
522
|
+
matches: matches.map((t) => ({
|
|
523
|
+
name: t.name,
|
|
524
|
+
databaseType: t.databaseType,
|
|
525
|
+
...(t.columns ? { columns: t.columns } : {}),
|
|
526
|
+
...(t.primaryKeys?.length ? { primaryKeys: t.primaryKeys } : {}),
|
|
527
|
+
...(t.foreignKeys?.length ? { foreignKeys: t.foreignKeys } : {}),
|
|
528
|
+
...(t.partitionKey ? { partitionKey: t.partitionKey } : {}),
|
|
529
|
+
...(t.sortKey ? { sortKey: t.sortKey } : {}),
|
|
530
|
+
...(t.estimatedCount !== undefined ? { estimatedCount: t.estimatedCount } : {}),
|
|
531
|
+
indexes: indexNamesFor(t.id),
|
|
532
|
+
})),
|
|
533
|
+
};
|
|
534
|
+
});
|
|
535
|
+
return toText({
|
|
536
|
+
note: 'Row data is never included.',
|
|
537
|
+
tables: results,
|
|
538
|
+
});
|
|
539
|
+
}));
|
|
490
540
|
mcp.registerTool('get_cache_overview', {
|
|
491
541
|
description: 'Returns all ElastiCache clusters with engine, version, node type, node count, in-transit and at-rest encryption status, replication group, and automatic failover state. Call this before writing cache client code (TLS is required when transit encryption is on — rediss:// for Redis) or when reviewing cache availability and security posture. Cached data is never read or included.',
|
|
492
542
|
inputSchema: z.object({}),
|
|
@@ -614,6 +664,7 @@ export function createServer(port = 3000) {
|
|
|
614
664
|
'get_cognito_overview',
|
|
615
665
|
'get_stream_details',
|
|
616
666
|
'get_cache_overview',
|
|
667
|
+
'get_table_schema',
|
|
617
668
|
],
|
|
618
669
|
}));
|
|
619
670
|
fastify.post('/mcp', async (request, reply) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
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": [
|