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 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 20 MCP tools ready
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 20 infrawise tools are available in Cursor's MCP panel.
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
- // Build maps for columns, indexes, primary keys
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' },
@@ -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({ id: nodeId, type: 'table', name: table.tableName, databaseType: 'dynamodb' });
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_')
@@ -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.14.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": [