mcp-database-inspector 2.0.1

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.
Files changed (105) hide show
  1. package/README.md +197 -0
  2. package/dist/database/connection.d.ts +13 -0
  3. package/dist/database/connection.d.ts.map +1 -0
  4. package/dist/database/connection.js +155 -0
  5. package/dist/database/connection.js.map +1 -0
  6. package/dist/database/manager.d.ts +28 -0
  7. package/dist/database/manager.d.ts.map +1 -0
  8. package/dist/database/manager.js +621 -0
  9. package/dist/database/manager.js.map +1 -0
  10. package/dist/database/postgres-connection.d.ts +10 -0
  11. package/dist/database/postgres-connection.d.ts.map +1 -0
  12. package/dist/database/postgres-connection.js +113 -0
  13. package/dist/database/postgres-connection.js.map +1 -0
  14. package/dist/database/types.d.ts +84 -0
  15. package/dist/database/types.d.ts.map +1 -0
  16. package/dist/database/types.js +6 -0
  17. package/dist/database/types.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +120 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/server.d.ts +14 -0
  23. package/dist/server.d.ts.map +1 -0
  24. package/dist/server.js +186 -0
  25. package/dist/server.js.map +1 -0
  26. package/dist/test-defaults.d.ts +2 -0
  27. package/dist/test-defaults.d.ts.map +1 -0
  28. package/dist/test-defaults.js +57 -0
  29. package/dist/test-defaults.js.map +1 -0
  30. package/dist/tools/analyze-query.d.ts +27 -0
  31. package/dist/tools/analyze-query.d.ts.map +1 -0
  32. package/dist/tools/analyze-query.js +71 -0
  33. package/dist/tools/analyze-query.js.map +1 -0
  34. package/dist/tools/execute-query.d.ts +33 -0
  35. package/dist/tools/execute-query.d.ts.map +1 -0
  36. package/dist/tools/execute-query.js +57 -0
  37. package/dist/tools/execute-query.js.map +1 -0
  38. package/dist/tools/get-foreign-keys.d.ts +38 -0
  39. package/dist/tools/get-foreign-keys.d.ts.map +1 -0
  40. package/dist/tools/get-foreign-keys.js +391 -0
  41. package/dist/tools/get-foreign-keys.js.map +1 -0
  42. package/dist/tools/get-indexes.d.ts +38 -0
  43. package/dist/tools/get-indexes.d.ts.map +1 -0
  44. package/dist/tools/get-indexes.js +472 -0
  45. package/dist/tools/get-indexes.js.map +1 -0
  46. package/dist/tools/information-schema-query.d.ts +33 -0
  47. package/dist/tools/information-schema-query.d.ts.map +1 -0
  48. package/dist/tools/information-schema-query.js +76 -0
  49. package/dist/tools/information-schema-query.js.map +1 -0
  50. package/dist/tools/inspect-table.d.ts +38 -0
  51. package/dist/tools/inspect-table.d.ts.map +1 -0
  52. package/dist/tools/inspect-table.js +351 -0
  53. package/dist/tools/inspect-table.js.map +1 -0
  54. package/dist/tools/list-databases.d.ts +14 -0
  55. package/dist/tools/list-databases.d.ts.map +1 -0
  56. package/dist/tools/list-databases.js +83 -0
  57. package/dist/tools/list-databases.js.map +1 -0
  58. package/dist/tools/list-tables.d.ts +19 -0
  59. package/dist/tools/list-tables.d.ts.map +1 -0
  60. package/dist/tools/list-tables.js +130 -0
  61. package/dist/tools/list-tables.js.map +1 -0
  62. package/dist/utils/errors.d.ts +32 -0
  63. package/dist/utils/errors.d.ts.map +1 -0
  64. package/dist/utils/errors.js +98 -0
  65. package/dist/utils/errors.js.map +1 -0
  66. package/dist/utils/logger.d.ts +28 -0
  67. package/dist/utils/logger.d.ts.map +1 -0
  68. package/dist/utils/logger.js +132 -0
  69. package/dist/utils/logger.js.map +1 -0
  70. package/dist/validators/input-validator.d.ts +76 -0
  71. package/dist/validators/input-validator.d.ts.map +1 -0
  72. package/dist/validators/input-validator.js +295 -0
  73. package/dist/validators/input-validator.js.map +1 -0
  74. package/dist/validators/query-validator.d.ts +19 -0
  75. package/dist/validators/query-validator.d.ts.map +1 -0
  76. package/dist/validators/query-validator.js +229 -0
  77. package/dist/validators/query-validator.js.map +1 -0
  78. package/enhanced_sql_prompt.md +324 -0
  79. package/examples/claude-config.json +23 -0
  80. package/examples/roo-config.json +16 -0
  81. package/package.json +42 -0
  82. package/src/database/connection.ts +165 -0
  83. package/src/database/manager.ts +682 -0
  84. package/src/database/postgres-connection.ts +123 -0
  85. package/src/database/types.ts +93 -0
  86. package/src/index.ts +136 -0
  87. package/src/server.ts +254 -0
  88. package/src/test-defaults.ts +63 -0
  89. package/src/tools/analyze-query.test.ts +100 -0
  90. package/src/tools/analyze-query.ts +112 -0
  91. package/src/tools/execute-query.ts +91 -0
  92. package/src/tools/get-foreign-keys.test.ts +51 -0
  93. package/src/tools/get-foreign-keys.ts +488 -0
  94. package/src/tools/get-indexes.test.ts +51 -0
  95. package/src/tools/get-indexes.ts +570 -0
  96. package/src/tools/information-schema-query.ts +125 -0
  97. package/src/tools/inspect-table.test.ts +59 -0
  98. package/src/tools/inspect-table.ts +440 -0
  99. package/src/tools/list-databases.ts +119 -0
  100. package/src/tools/list-tables.ts +181 -0
  101. package/src/utils/errors.ts +103 -0
  102. package/src/utils/logger.ts +158 -0
  103. package/src/validators/input-validator.ts +318 -0
  104. package/src/validators/query-validator.ts +267 -0
  105. package/tsconfig.json +30 -0
@@ -0,0 +1,181 @@
1
+ import { z } from 'zod';
2
+ import { DatabaseManager } from '../database/manager.js';
3
+ import { InputValidator } from '../validators/input-validator.js';
4
+ import { Logger } from '../utils/logger.js';
5
+ import { ToolError, createErrorResponse } from '../utils/errors.js';
6
+
7
+ // Tool schema
8
+ const ListTablesArgsSchema = z.object({
9
+ database: z.string().min(1, 'Database name is required')
10
+ });
11
+
12
+ export interface ListTablesTool {
13
+ name: 'list_tables';
14
+ description: 'List all tables in the specified database with metadata';
15
+ inputSchema: {
16
+ type: 'object';
17
+ properties: {
18
+ database: {
19
+ type: 'string';
20
+ description: 'Name of the database to list tables from';
21
+ };
22
+ };
23
+ required: ['database'];
24
+ };
25
+ }
26
+
27
+ export const listTablesToolDefinition: ListTablesTool = {
28
+ name: 'list_tables',
29
+ description: 'List all tables in the specified database with metadata',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ database: {
34
+ type: 'string',
35
+ description: 'Name of the database to list tables from'
36
+ }
37
+ },
38
+ required: ['database']
39
+ }
40
+ };
41
+
42
+ export async function handleListTables(
43
+ args: unknown,
44
+ dbManager: DatabaseManager
45
+ ): Promise<any> {
46
+ try {
47
+ Logger.info('Executing list_tables tool');
48
+
49
+ // Validate arguments
50
+ const validationResult = ListTablesArgsSchema.safeParse(args);
51
+ if (!validationResult.success) {
52
+ Logger.warn('Invalid arguments for list_tables', validationResult.error);
53
+ throw new ToolError(
54
+ `Invalid arguments: ${validationResult.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
55
+ 'list_tables'
56
+ );
57
+ }
58
+
59
+ const { database } = validationResult.data;
60
+
61
+ // Sanitize database name
62
+ const sanitizedDatabase = InputValidator.sanitizeString(database);
63
+
64
+ // Validate database name format
65
+ const dbNameValidation = InputValidator.validateDatabaseName(sanitizedDatabase);
66
+ if (!dbNameValidation.isValid) {
67
+ throw new ToolError(
68
+ `Invalid database name: ${dbNameValidation.error}`,
69
+ 'list_tables'
70
+ );
71
+ }
72
+
73
+ Logger.info(`Listing tables for database: ${sanitizedDatabase}`);
74
+ Logger.time('list_tables_execution');
75
+
76
+ // Get tables from database
77
+ const tables = await dbManager.getTables(sanitizedDatabase);
78
+
79
+ Logger.timeEnd('list_tables_execution');
80
+ Logger.info(`Found ${tables.length} tables in database: ${sanitizedDatabase}`);
81
+
82
+ // Group tables by type
83
+ const tablesByType = tables.reduce((acc, table) => {
84
+ const type = table.tableType;
85
+ if (!acc[type]) {
86
+ acc[type] = [];
87
+ }
88
+ acc[type].push(table);
89
+ return acc;
90
+ }, {} as Record<string, typeof tables>);
91
+
92
+ // Calculate statistics
93
+ const stats = {
94
+ totalTables: tables.length,
95
+ baseTables: (tablesByType['BASE TABLE'] || []).length,
96
+ views: (tablesByType['VIEW'] || []).length,
97
+ systemTables: (tablesByType['SYSTEM TABLE'] || []).length,
98
+ totalEstimatedRows: tables.reduce((sum, table) => sum + (table.tableRows || 0), 0)
99
+ };
100
+
101
+ // Format response
102
+ const response = {
103
+ database: sanitizedDatabase,
104
+ tables: tables.map(table => ({
105
+ name: table.tableName,
106
+ type: table.tableType,
107
+ engine: table.engine,
108
+ estimatedRows: table.tableRows,
109
+ comment: table.tableComment,
110
+ category: table.tableType === 'BASE TABLE' ? 'table' :
111
+ table.tableType === 'VIEW' ? 'view' :
112
+ 'system'
113
+ })),
114
+ statistics: stats,
115
+ tablesByType: Object.keys(tablesByType).map(type => ({
116
+ type,
117
+ count: tablesByType[type].length,
118
+ tables: tablesByType[type].map(t => t.tableName)
119
+ })),
120
+ summary: {
121
+ hasData: tables.length > 0,
122
+ message: tables.length === 0
123
+ ? `No tables found in database '${sanitizedDatabase}'. Database may be empty or inaccessible.`
124
+ : `Found ${tables.length} table(s) in database '${sanitizedDatabase}': ${stats.baseTables} base tables, ${stats.views} views.`
125
+ }
126
+ };
127
+
128
+ Logger.debug('list_tables completed successfully', {
129
+ database: sanitizedDatabase,
130
+ tableCount: tables.length,
131
+ baseTables: stats.baseTables,
132
+ views: stats.views
133
+ });
134
+
135
+ return {
136
+ content: [{
137
+ type: 'text',
138
+ text: JSON.stringify(response, null, 2)
139
+ }]
140
+ };
141
+
142
+ } catch (error) {
143
+ Logger.error('Error in list_tables tool', error);
144
+
145
+ if (error instanceof ToolError) {
146
+ throw error;
147
+ }
148
+
149
+ throw new ToolError(
150
+ `Failed to list tables: ${error instanceof Error ? error.message : 'Unknown error'}`,
151
+ 'list_tables',
152
+ error instanceof Error ? error : undefined
153
+ );
154
+ }
155
+ }
156
+
157
+ export async function getTablesSummary(
158
+ dbManager: DatabaseManager,
159
+ database: string
160
+ ): Promise<string> {
161
+ try {
162
+ const tables = await dbManager.getTables(database);
163
+ const baseTables = tables.filter(t => t.tableType === 'BASE TABLE').length;
164
+ const views = tables.filter(t => t.tableType === 'VIEW').length;
165
+
166
+ if (tables.length === 0) {
167
+ return `Database '${database}' has no tables`;
168
+ }
169
+
170
+ const parts = [];
171
+ if (baseTables > 0) parts.push(`${baseTables} table(s)`);
172
+ if (views > 0) parts.push(`${views} view(s)`);
173
+
174
+ return `Database '${database}' contains ${parts.join(', ')}: ${
175
+ tables.slice(0, 5).map(t => t.tableName).join(', ')
176
+ }${tables.length > 5 ? `, and ${tables.length - 5} more` : ''}`;
177
+ } catch (error) {
178
+ Logger.error(`Error getting table summary for ${database}`, error);
179
+ return `Error retrieving tables for database '${database}'`;
180
+ }
181
+ }
@@ -0,0 +1,103 @@
1
+ export class DatabaseError extends Error {
2
+ constructor(message: string, public readonly cause?: Error) {
3
+ super(message);
4
+ this.name = 'DatabaseError';
5
+
6
+ if (cause) {
7
+ this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
8
+ }
9
+ }
10
+ }
11
+
12
+ export class ValidationError extends Error {
13
+ constructor(message: string, public readonly field?: string) {
14
+ super(message);
15
+ this.name = 'ValidationError';
16
+ }
17
+ }
18
+
19
+ export class ConnectionError extends DatabaseError {
20
+ constructor(message: string, public readonly host?: string, public readonly port?: number, cause?: Error) {
21
+ super(message, cause);
22
+ this.name = 'ConnectionError';
23
+ }
24
+ }
25
+
26
+ export class QueryError extends DatabaseError {
27
+ constructor(message: string, public readonly query?: string, cause?: Error) {
28
+ super(message, cause);
29
+ this.name = 'QueryError';
30
+ }
31
+ }
32
+
33
+ export class ConfigurationError extends Error {
34
+ constructor(message: string, public readonly configKey?: string) {
35
+ super(message);
36
+ this.name = 'ConfigurationError';
37
+ }
38
+ }
39
+
40
+ export class ToolError extends Error {
41
+ constructor(message: string, public readonly toolName?: string, cause?: Error) {
42
+ super(message);
43
+ this.name = 'ToolError';
44
+
45
+ if (cause) {
46
+ this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
47
+ }
48
+ }
49
+ }
50
+
51
+ export function isRecoverableError(error: Error): boolean {
52
+ // Determine if an error is recoverable (temporary) or fatal
53
+ if (error instanceof ConnectionError) {
54
+ // Network timeouts, temporary connection issues
55
+ return error.message.includes('timeout') ||
56
+ error.message.includes('ECONNREFUSED') ||
57
+ error.message.includes('ENOTFOUND');
58
+ }
59
+
60
+ if (error instanceof QueryError) {
61
+ // Syntax errors are not recoverable, but timeouts might be
62
+ return error.message.includes('timeout');
63
+ }
64
+
65
+ return false;
66
+ }
67
+
68
+ export function sanitizeErrorMessage(error: Error): string {
69
+ // Remove sensitive information from error messages
70
+ let message = error.message;
71
+
72
+ // Remove password information from connection strings
73
+ message = message.replace(/password=[^&\s]+/gi, 'password=***');
74
+ message = message.replace(/:([^@\s]+)@/g, ':***@');
75
+
76
+ return message;
77
+ }
78
+
79
+ export function createErrorResponse(error: Error): { error: string; code?: string } {
80
+ const sanitized = sanitizeErrorMessage(error);
81
+
82
+ if (error instanceof ValidationError) {
83
+ return { error: sanitized, code: 'VALIDATION_ERROR' };
84
+ }
85
+
86
+ if (error instanceof ConnectionError) {
87
+ return { error: sanitized, code: 'CONNECTION_ERROR' };
88
+ }
89
+
90
+ if (error instanceof QueryError) {
91
+ return { error: sanitized, code: 'QUERY_ERROR' };
92
+ }
93
+
94
+ if (error instanceof DatabaseError) {
95
+ return { error: sanitized, code: 'DATABASE_ERROR' };
96
+ }
97
+
98
+ if (error instanceof ToolError) {
99
+ return { error: sanitized, code: 'TOOL_ERROR' };
100
+ }
101
+
102
+ return { error: sanitized, code: 'UNKNOWN_ERROR' };
103
+ }
@@ -0,0 +1,158 @@
1
+ enum LogLevel {
2
+ ERROR = 0,
3
+ WARN = 1,
4
+ INFO = 2,
5
+ DEBUG = 3,
6
+ TRACE = 4
7
+ }
8
+
9
+ interface LogEntry {
10
+ timestamp: string;
11
+ level: string;
12
+ message: string;
13
+ data?: any;
14
+ }
15
+
16
+ export class Logger {
17
+ private static logLevel: LogLevel = LogLevel.INFO;
18
+ private static logs: LogEntry[] = [];
19
+ private static maxLogs = 1000;
20
+
21
+ static setLogLevel(level: 'error' | 'warn' | 'info' | 'debug' | 'trace'): void {
22
+ const levelMap = {
23
+ error: LogLevel.ERROR,
24
+ warn: LogLevel.WARN,
25
+ info: LogLevel.INFO,
26
+ debug: LogLevel.DEBUG,
27
+ trace: LogLevel.TRACE
28
+ };
29
+ this.logLevel = levelMap[level] ?? LogLevel.INFO;
30
+ }
31
+
32
+ static getLogLevel(): string {
33
+ const levelNames = ['error', 'warn', 'info', 'debug', 'trace'];
34
+ return levelNames[this.logLevel] || 'info';
35
+ }
36
+
37
+ private static shouldLog(level: LogLevel): boolean {
38
+ return level <= this.logLevel;
39
+ }
40
+
41
+ private static addLog(level: string, message: string, data?: any): void {
42
+ const entry: LogEntry = {
43
+ timestamp: new Date().toISOString(),
44
+ level,
45
+ message,
46
+ data
47
+ };
48
+
49
+ this.logs.push(entry);
50
+
51
+ // Keep only the most recent logs
52
+ if (this.logs.length > this.maxLogs) {
53
+ this.logs = this.logs.slice(-this.maxLogs);
54
+ }
55
+
56
+ // Output to stderr to avoid interfering with MCP protocol on stdout
57
+ const logMessage = this.formatLog(entry);
58
+ console.error(logMessage);
59
+ }
60
+
61
+ private static formatLog(entry: LogEntry): string {
62
+ const timestamp = entry.timestamp.split('T')[1].split('.')[0]; // HH:MM:SS format
63
+ let message = `[${timestamp}] ${entry.level.toUpperCase()}: ${entry.message}`;
64
+
65
+ if (entry.data !== undefined) {
66
+ message += ` ${JSON.stringify(entry.data)}`;
67
+ }
68
+
69
+ return message;
70
+ }
71
+
72
+ static error(message: string, data?: any): void {
73
+ if (this.shouldLog(LogLevel.ERROR)) {
74
+ this.addLog('error', message, data);
75
+ }
76
+ }
77
+
78
+ static warn(message: string, data?: any): void {
79
+ if (this.shouldLog(LogLevel.WARN)) {
80
+ this.addLog('warn', message, data);
81
+ }
82
+ }
83
+
84
+ static info(message: string, data?: any): void {
85
+ if (this.shouldLog(LogLevel.INFO)) {
86
+ this.addLog('info', message, data);
87
+ }
88
+ }
89
+
90
+ static debug(message: string, data?: any): void {
91
+ if (this.shouldLog(LogLevel.DEBUG)) {
92
+ this.addLog('debug', message, data);
93
+ }
94
+ }
95
+
96
+ static trace(message: string, data?: any): void {
97
+ if (this.shouldLog(LogLevel.TRACE)) {
98
+ this.addLog('trace', message, data);
99
+ }
100
+ }
101
+
102
+ static getLogs(limit?: number): LogEntry[] {
103
+ if (limit && limit > 0) {
104
+ return this.logs.slice(-limit);
105
+ }
106
+ return [...this.logs];
107
+ }
108
+
109
+ static clearLogs(): void {
110
+ this.logs = [];
111
+ }
112
+
113
+ // Performance timing utilities
114
+ static time(label: string): void {
115
+ if (this.shouldLog(LogLevel.DEBUG)) {
116
+ console.time(`[TIMER] ${label}`);
117
+ }
118
+ }
119
+
120
+ static timeEnd(label: string): void {
121
+ if (this.shouldLog(LogLevel.DEBUG)) {
122
+ console.timeEnd(`[TIMER] ${label}`);
123
+ }
124
+ }
125
+
126
+ // Safe logging that won't throw errors
127
+ static safeLog(level: 'error' | 'warn' | 'info' | 'debug' | 'trace', message: string, data?: any): void {
128
+ try {
129
+ switch (level) {
130
+ case 'error':
131
+ this.error(message, data);
132
+ break;
133
+ case 'warn':
134
+ this.warn(message, data);
135
+ break;
136
+ case 'info':
137
+ this.info(message, data);
138
+ break;
139
+ case 'debug':
140
+ this.debug(message, data);
141
+ break;
142
+ case 'trace':
143
+ this.trace(message, data);
144
+ break;
145
+ }
146
+ } catch (error) {
147
+ // Fallback to console.error if our logging fails
148
+ console.error(`Logging failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
149
+ console.error(`Original message: ${message}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ // Initialize from environment variable
155
+ const envLogLevel = process.env.LOG_LEVEL?.toLowerCase();
156
+ if (envLogLevel && ['error', 'warn', 'info', 'debug', 'trace'].includes(envLogLevel)) {
157
+ Logger.setLogLevel(envLogLevel as any);
158
+ }