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.
- package/README.md +197 -0
- package/dist/database/connection.d.ts +13 -0
- package/dist/database/connection.d.ts.map +1 -0
- package/dist/database/connection.js +155 -0
- package/dist/database/connection.js.map +1 -0
- package/dist/database/manager.d.ts +28 -0
- package/dist/database/manager.d.ts.map +1 -0
- package/dist/database/manager.js +621 -0
- package/dist/database/manager.js.map +1 -0
- package/dist/database/postgres-connection.d.ts +10 -0
- package/dist/database/postgres-connection.d.ts.map +1 -0
- package/dist/database/postgres-connection.js +113 -0
- package/dist/database/postgres-connection.js.map +1 -0
- package/dist/database/types.d.ts +84 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +6 -0
- package/dist/database/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +186 -0
- package/dist/server.js.map +1 -0
- package/dist/test-defaults.d.ts +2 -0
- package/dist/test-defaults.d.ts.map +1 -0
- package/dist/test-defaults.js +57 -0
- package/dist/test-defaults.js.map +1 -0
- package/dist/tools/analyze-query.d.ts +27 -0
- package/dist/tools/analyze-query.d.ts.map +1 -0
- package/dist/tools/analyze-query.js +71 -0
- package/dist/tools/analyze-query.js.map +1 -0
- package/dist/tools/execute-query.d.ts +33 -0
- package/dist/tools/execute-query.d.ts.map +1 -0
- package/dist/tools/execute-query.js +57 -0
- package/dist/tools/execute-query.js.map +1 -0
- package/dist/tools/get-foreign-keys.d.ts +38 -0
- package/dist/tools/get-foreign-keys.d.ts.map +1 -0
- package/dist/tools/get-foreign-keys.js +391 -0
- package/dist/tools/get-foreign-keys.js.map +1 -0
- package/dist/tools/get-indexes.d.ts +38 -0
- package/dist/tools/get-indexes.d.ts.map +1 -0
- package/dist/tools/get-indexes.js +472 -0
- package/dist/tools/get-indexes.js.map +1 -0
- package/dist/tools/information-schema-query.d.ts +33 -0
- package/dist/tools/information-schema-query.d.ts.map +1 -0
- package/dist/tools/information-schema-query.js +76 -0
- package/dist/tools/information-schema-query.js.map +1 -0
- package/dist/tools/inspect-table.d.ts +38 -0
- package/dist/tools/inspect-table.d.ts.map +1 -0
- package/dist/tools/inspect-table.js +351 -0
- package/dist/tools/inspect-table.js.map +1 -0
- package/dist/tools/list-databases.d.ts +14 -0
- package/dist/tools/list-databases.d.ts.map +1 -0
- package/dist/tools/list-databases.js +83 -0
- package/dist/tools/list-databases.js.map +1 -0
- package/dist/tools/list-tables.d.ts +19 -0
- package/dist/tools/list-tables.d.ts.map +1 -0
- package/dist/tools/list-tables.js +130 -0
- package/dist/tools/list-tables.js.map +1 -0
- package/dist/utils/errors.d.ts +32 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +98 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +28 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +132 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/validators/input-validator.d.ts +76 -0
- package/dist/validators/input-validator.d.ts.map +1 -0
- package/dist/validators/input-validator.js +295 -0
- package/dist/validators/input-validator.js.map +1 -0
- package/dist/validators/query-validator.d.ts +19 -0
- package/dist/validators/query-validator.d.ts.map +1 -0
- package/dist/validators/query-validator.js +229 -0
- package/dist/validators/query-validator.js.map +1 -0
- package/enhanced_sql_prompt.md +324 -0
- package/examples/claude-config.json +23 -0
- package/examples/roo-config.json +16 -0
- package/package.json +42 -0
- package/src/database/connection.ts +165 -0
- package/src/database/manager.ts +682 -0
- package/src/database/postgres-connection.ts +123 -0
- package/src/database/types.ts +93 -0
- package/src/index.ts +136 -0
- package/src/server.ts +254 -0
- package/src/test-defaults.ts +63 -0
- package/src/tools/analyze-query.test.ts +100 -0
- package/src/tools/analyze-query.ts +112 -0
- package/src/tools/execute-query.ts +91 -0
- package/src/tools/get-foreign-keys.test.ts +51 -0
- package/src/tools/get-foreign-keys.ts +488 -0
- package/src/tools/get-indexes.test.ts +51 -0
- package/src/tools/get-indexes.ts +570 -0
- package/src/tools/information-schema-query.ts +125 -0
- package/src/tools/inspect-table.test.ts +59 -0
- package/src/tools/inspect-table.ts +440 -0
- package/src/tools/list-databases.ts +119 -0
- package/src/tools/list-tables.ts +181 -0
- package/src/utils/errors.ts +103 -0
- package/src/utils/logger.ts +158 -0
- package/src/validators/input-validator.ts +318 -0
- package/src/validators/query-validator.ts +267 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
import { DatabaseConnectionOptions, QueryResult, DatabaseType } from './types.js';
|
|
4
|
+
import { Logger } from '../utils/logger.js';
|
|
5
|
+
import { DatabaseError } from '../utils/errors.js';
|
|
6
|
+
|
|
7
|
+
export class PostgresConnection {
|
|
8
|
+
static parseConnectionUrl(url: string): DatabaseConnectionOptions {
|
|
9
|
+
try {
|
|
10
|
+
const parsedUrl = new URL(url);
|
|
11
|
+
|
|
12
|
+
if (parsedUrl.protocol !== 'postgresql:' && parsedUrl.protocol !== 'postgres:') {
|
|
13
|
+
throw new DatabaseError(`Unsupported protocol: ${parsedUrl.protocol}. Only 'postgresql:' or 'postgres:' is supported.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const options: DatabaseConnectionOptions = {
|
|
17
|
+
type: DatabaseType.PostgreSQL,
|
|
18
|
+
host: parsedUrl.hostname,
|
|
19
|
+
port: parsedUrl.port ? parseInt(parsedUrl.port) : 5432,
|
|
20
|
+
user: parsedUrl.username,
|
|
21
|
+
password: parsedUrl.password,
|
|
22
|
+
database: parsedUrl.pathname.slice(1), // Remove leading slash
|
|
23
|
+
reconnect: true
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Parse SSL settings from query parameters
|
|
27
|
+
const searchParams = parsedUrl.searchParams;
|
|
28
|
+
if (searchParams.has('ssl')) {
|
|
29
|
+
const sslValue = searchParams.get('ssl');
|
|
30
|
+
if (sslValue === 'true' || sslValue === '1') {
|
|
31
|
+
options.ssl = true;
|
|
32
|
+
} else if (sslValue === 'false' || sslValue === '0') {
|
|
33
|
+
options.ssl = false;
|
|
34
|
+
} else if (sslValue === 'no-verify') {
|
|
35
|
+
options.ssl = { rejectUnauthorized: false };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return options;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new DatabaseError(`Invalid connection URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async createClient(options: DatabaseConnectionOptions): Promise<pg.Client> {
|
|
46
|
+
try {
|
|
47
|
+
Logger.info(`Connecting to PostgreSQL database at ${options.host}:${options.port}/${options.database}`);
|
|
48
|
+
|
|
49
|
+
const clientConfig: pg.ClientConfig = {
|
|
50
|
+
host: options.host,
|
|
51
|
+
port: options.port,
|
|
52
|
+
user: options.user,
|
|
53
|
+
password: options.password,
|
|
54
|
+
database: options.database,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (options.ssl) {
|
|
58
|
+
clientConfig.ssl = options.ssl;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const client = new pg.Client(clientConfig);
|
|
62
|
+
await client.connect();
|
|
63
|
+
|
|
64
|
+
// Test the connection
|
|
65
|
+
await client.query('SELECT 1');
|
|
66
|
+
|
|
67
|
+
Logger.info(`Successfully connected to PostgreSQL database: ${options.database}`);
|
|
68
|
+
return client;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
Logger.error(`Failed to connect to PostgreSQL database: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
71
|
+
throw new DatabaseError(`Connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static async executeQuery(client: pg.Client, query: string, params?: any[]): Promise<QueryResult> {
|
|
76
|
+
try {
|
|
77
|
+
Logger.debug(`Executing PostgreSQL query: ${query.substring(0, 100)}${query.length > 100 ? '...' : ''}`);
|
|
78
|
+
|
|
79
|
+
// PostgreSQL uses $1, $2 for parameters, but if we receive ? from MySQL style queries
|
|
80
|
+
// we might need translation, but for now we expect raw compatibility or manual handling
|
|
81
|
+
const res = await client.query(query, params);
|
|
82
|
+
|
|
83
|
+
const result: QueryResult = {
|
|
84
|
+
rows: res.rows,
|
|
85
|
+
fields: res.fields.map((f: any) => ({ name: f.name, type: f.dataTypeID })),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
Logger.debug(`Query executed successfully, returned ${result.rows.length} rows`);
|
|
89
|
+
return result;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
Logger.error(
|
|
92
|
+
`PostgreSQL query execution failed: ${error instanceof Error ? error.message : 'Unknown error'}\n` +
|
|
93
|
+
`Full SQL: ${query}\n` +
|
|
94
|
+
`Params: ${JSON.stringify(params)}`
|
|
95
|
+
);
|
|
96
|
+
throw new DatabaseError(`Query execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static async closeConnection(client: pg.Client): Promise<void> {
|
|
101
|
+
try {
|
|
102
|
+
await client.end();
|
|
103
|
+
Logger.info('PostgreSQL connection closed successfully');
|
|
104
|
+
} catch (error) {
|
|
105
|
+
Logger.warn(`Error closing PostgreSQL connection: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static async testConnection(options: DatabaseConnectionOptions): Promise<boolean> {
|
|
110
|
+
let client: pg.Client | null = null;
|
|
111
|
+
try {
|
|
112
|
+
client = await this.createClient(options);
|
|
113
|
+
return true;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
Logger.warn(`PostgreSQL connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
116
|
+
return false;
|
|
117
|
+
} finally {
|
|
118
|
+
if (client) {
|
|
119
|
+
await this.closeConnection(client);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export enum DatabaseType {
|
|
2
|
+
MySQL = 'mysql',
|
|
3
|
+
PostgreSQL = 'postgresql'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface DatabaseConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
url: string;
|
|
9
|
+
type: DatabaseType;
|
|
10
|
+
connection: any | null; // Can be mysql.Connection or pg.Client
|
|
11
|
+
lastUsed: Date;
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
username: string;
|
|
15
|
+
password?: string;
|
|
16
|
+
database: string;
|
|
17
|
+
ssl?: boolean | any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DatabaseInfo {
|
|
21
|
+
name: string;
|
|
22
|
+
type: DatabaseType;
|
|
23
|
+
connected: boolean;
|
|
24
|
+
lastUsed: Date;
|
|
25
|
+
host: string;
|
|
26
|
+
database: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TableInfo {
|
|
30
|
+
tableName: string;
|
|
31
|
+
tableType: string;
|
|
32
|
+
engine?: string;
|
|
33
|
+
tableRows?: number;
|
|
34
|
+
tableComment?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ColumnInfo {
|
|
38
|
+
columnName: string;
|
|
39
|
+
dataType: string;
|
|
40
|
+
isNullable: string;
|
|
41
|
+
columnDefault?: any;
|
|
42
|
+
isPrimaryKey: boolean;
|
|
43
|
+
isAutoIncrement: boolean;
|
|
44
|
+
columnComment?: string;
|
|
45
|
+
characterMaximumLength?: number;
|
|
46
|
+
numericPrecision?: number;
|
|
47
|
+
numericScale?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ForeignKeyInfo {
|
|
51
|
+
constraintName: string;
|
|
52
|
+
tableName: string;
|
|
53
|
+
columnName: string;
|
|
54
|
+
referencedTableName: string;
|
|
55
|
+
referencedColumnName: string;
|
|
56
|
+
updateRule?: string;
|
|
57
|
+
deleteRule?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface IndexInfo {
|
|
61
|
+
tableName: string;
|
|
62
|
+
indexName: string;
|
|
63
|
+
columnName: string;
|
|
64
|
+
nonUnique: boolean;
|
|
65
|
+
indexType?: string;
|
|
66
|
+
cardinality?: number;
|
|
67
|
+
subPart?: number;
|
|
68
|
+
nullable: boolean;
|
|
69
|
+
isPrimary?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface QueryResult {
|
|
73
|
+
rows: any[];
|
|
74
|
+
fields: any[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ValidationResult {
|
|
78
|
+
isValid: boolean;
|
|
79
|
+
error?: string;
|
|
80
|
+
warnings?: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface DatabaseConnectionOptions {
|
|
84
|
+
type: DatabaseType;
|
|
85
|
+
host: string;
|
|
86
|
+
port: number;
|
|
87
|
+
user: string;
|
|
88
|
+
password: string;
|
|
89
|
+
database: string;
|
|
90
|
+
ssl?: boolean | object;
|
|
91
|
+
// timeout and acquireTimeout are not valid for Connection configs and should not be used
|
|
92
|
+
reconnect?: boolean;
|
|
93
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { DatabaseInspectorServer } from './server.js';
|
|
2
|
+
import { Logger } from './utils/logger.js';
|
|
3
|
+
import { InputValidator } from './validators/input-validator.js';
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
try {
|
|
7
|
+
// Parse command line arguments
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
if (args.length === 0) {
|
|
11
|
+
console.error(`
|
|
12
|
+
Usage: mcp-database-inspector <database_url1> [database_url2] [database_url3] ...
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
# Single MySQL database
|
|
16
|
+
mcp-database-inspector "mysql://user:password@localhost:3306/mydb"
|
|
17
|
+
|
|
18
|
+
# Single PostgreSQL database
|
|
19
|
+
mcp-database-inspector "postgresql://user:password@localhost:5432/mydb"
|
|
20
|
+
|
|
21
|
+
# Mixed databases
|
|
22
|
+
mcp-database-inspector \\
|
|
23
|
+
"mysql://user:pass@mysql-host:3306/orders" \\
|
|
24
|
+
"postgresql://user:pass@pg-host:5432/analytics"
|
|
25
|
+
|
|
26
|
+
# With SSL
|
|
27
|
+
mcp-database-inspector "mysql://user:pass@localhost:3306/db?ssl=true"
|
|
28
|
+
|
|
29
|
+
Environment Variables:
|
|
30
|
+
LOG_LEVEL - Set logging level (error, warn, info, debug, trace)
|
|
31
|
+
Default: info
|
|
32
|
+
|
|
33
|
+
Database URL Format:
|
|
34
|
+
mysql://username:password@hostname:port/database?options
|
|
35
|
+
postgresql://username:password@hostname:port/database?options
|
|
36
|
+
|
|
37
|
+
Supported Options:
|
|
38
|
+
- ssl=true/false - Enable/disable SSL connection
|
|
39
|
+
- timeout=30 - Connection timeout in seconds (default: 30)
|
|
40
|
+
`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Set log level from environment
|
|
45
|
+
const logLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
46
|
+
if (logLevel && ['error', 'warn', 'info', 'debug', 'trace'].includes(logLevel)) {
|
|
47
|
+
Logger.setLogLevel(logLevel as any);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Logger.info('MCP Database Inspector starting...');
|
|
51
|
+
Logger.info(`Log level: ${Logger.getLogLevel()}`);
|
|
52
|
+
|
|
53
|
+
// Validate connection URLs
|
|
54
|
+
const connectionUrls: string[] = [];
|
|
55
|
+
for (const url of args) {
|
|
56
|
+
Logger.debug(`Validating connection URL: ${InputValidator.sanitizeForLogging(url)}`);
|
|
57
|
+
|
|
58
|
+
const validation = InputValidator.validateConnectionUrl(url);
|
|
59
|
+
if (!validation.isValid) {
|
|
60
|
+
Logger.error(`Invalid connection URL: ${validation.error}`);
|
|
61
|
+
console.error(`Error: Invalid connection URL - ${validation.error}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
connectionUrls.push(url);
|
|
66
|
+
Logger.debug(`Connection URL validated successfully`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Logger.info(`Validated ${connectionUrls.length} connection URL(s)`);
|
|
70
|
+
|
|
71
|
+
// Create and initialize server
|
|
72
|
+
const server = new DatabaseInspectorServer();
|
|
73
|
+
|
|
74
|
+
// Set up graceful shutdown
|
|
75
|
+
let isShuttingDown = false;
|
|
76
|
+
const shutdownHandler = async () => {
|
|
77
|
+
if (isShuttingDown) return;
|
|
78
|
+
isShuttingDown = true;
|
|
79
|
+
|
|
80
|
+
Logger.info('Shutting down...');
|
|
81
|
+
try {
|
|
82
|
+
await server.shutdown();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
Logger.error('Error during shutdown:', error);
|
|
85
|
+
}
|
|
86
|
+
process.exit(0);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
process.on('SIGINT', shutdownHandler);
|
|
90
|
+
process.on('SIGTERM', shutdownHandler);
|
|
91
|
+
|
|
92
|
+
// Initialize with database connections
|
|
93
|
+
Logger.info('Initializing database connections...');
|
|
94
|
+
await server.initialize(connectionUrls);
|
|
95
|
+
|
|
96
|
+
// Get server info for logging
|
|
97
|
+
const serverInfo = await server.getServerInfo();
|
|
98
|
+
Logger.info('Server initialization complete', {
|
|
99
|
+
connectedDatabases: serverInfo.connectedDatabases,
|
|
100
|
+
databases: serverInfo.databases.map((db: any) => db.name),
|
|
101
|
+
status: serverInfo.status
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Start the server
|
|
105
|
+
await server.run();
|
|
106
|
+
|
|
107
|
+
// This point should not be reached as the server runs indefinitely
|
|
108
|
+
Logger.warn('Server.run() returned unexpectedly');
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
Logger.error('Fatal error during startup:', error);
|
|
112
|
+
console.error(`Fatal error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle unhandled promise rejections
|
|
118
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
119
|
+
Logger.error('Unhandled Promise Rejection:', { promise, reason });
|
|
120
|
+
console.error('Unhandled Promise Rejection:', reason);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Handle uncaught exceptions
|
|
125
|
+
process.on('uncaughtException', (error) => {
|
|
126
|
+
Logger.error('Uncaught Exception:', error);
|
|
127
|
+
console.error('Uncaught Exception:', error.message);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Start the application
|
|
132
|
+
main().catch((error) => {
|
|
133
|
+
Logger.error('Error in main function:', error);
|
|
134
|
+
console.error('Startup error:', error.message);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
});
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
|
|
8
|
+
import { DatabaseManager } from './database/manager.js';
|
|
9
|
+
import { Logger } from './utils/logger.js';
|
|
10
|
+
import { createErrorResponse, ToolError } from './utils/errors.js';
|
|
11
|
+
|
|
12
|
+
// Import tool handlers
|
|
13
|
+
import {
|
|
14
|
+
listDatabasesToolDefinition,
|
|
15
|
+
handleListDatabases
|
|
16
|
+
} from './tools/list-databases.js';
|
|
17
|
+
import {
|
|
18
|
+
listTablesToolDefinition,
|
|
19
|
+
handleListTables
|
|
20
|
+
} from './tools/list-tables.js';
|
|
21
|
+
import {
|
|
22
|
+
inspectTableToolDefinition,
|
|
23
|
+
handleInspectTable
|
|
24
|
+
} from './tools/inspect-table.js';
|
|
25
|
+
import {
|
|
26
|
+
getForeignKeysToolDefinition,
|
|
27
|
+
handleGetForeignKeys
|
|
28
|
+
} from './tools/get-foreign-keys.js';
|
|
29
|
+
import {
|
|
30
|
+
getIndexesToolDefinition,
|
|
31
|
+
handleGetIndexes
|
|
32
|
+
} from './tools/get-indexes.js';
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
informationSchemaQueryToolDefinition,
|
|
36
|
+
handleInformationSchemaQuery
|
|
37
|
+
} from './tools/information-schema-query.js';
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
executeQueryToolDefinition,
|
|
41
|
+
handleExecuteQuery
|
|
42
|
+
} from './tools/execute-query.js';
|
|
43
|
+
|
|
44
|
+
import {
|
|
45
|
+
analyzeQueryToolDefinition,
|
|
46
|
+
handleAnalyzeQuery
|
|
47
|
+
} from './tools/analyze-query.js';
|
|
48
|
+
|
|
49
|
+
export class DatabaseInspectorServer {
|
|
50
|
+
private server: Server;
|
|
51
|
+
private dbManager: DatabaseManager;
|
|
52
|
+
|
|
53
|
+
constructor() {
|
|
54
|
+
this.server = new Server(
|
|
55
|
+
{
|
|
56
|
+
name: 'mcp-database-inspector',
|
|
57
|
+
version: '2.0.1',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
capabilities: {
|
|
61
|
+
tools: {},
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
this.dbManager = new DatabaseManager();
|
|
67
|
+
this.setupHandlers();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private setupHandlers(): void {
|
|
71
|
+
// List available tools
|
|
72
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
73
|
+
Logger.debug('Received list_tools request');
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
tools: [
|
|
77
|
+
listDatabasesToolDefinition,
|
|
78
|
+
listTablesToolDefinition,
|
|
79
|
+
inspectTableToolDefinition,
|
|
80
|
+
getForeignKeysToolDefinition,
|
|
81
|
+
getIndexesToolDefinition,
|
|
82
|
+
informationSchemaQueryToolDefinition,
|
|
83
|
+
executeQueryToolDefinition,
|
|
84
|
+
analyzeQueryToolDefinition
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Handle tool calls
|
|
90
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
91
|
+
const { name, arguments: args } = request.params;
|
|
92
|
+
|
|
93
|
+
Logger.info(`Received tool call: ${name}`);
|
|
94
|
+
Logger.debug(`Tool arguments:`, args);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
switch (name) {
|
|
98
|
+
case 'list_databases':
|
|
99
|
+
return await handleListDatabases(args, this.dbManager);
|
|
100
|
+
|
|
101
|
+
case 'list_tables':
|
|
102
|
+
return await handleListTables(args, this.dbManager);
|
|
103
|
+
|
|
104
|
+
case 'inspect_table':
|
|
105
|
+
return await handleInspectTable(args, this.dbManager);
|
|
106
|
+
|
|
107
|
+
case 'get_foreign_keys':
|
|
108
|
+
return await handleGetForeignKeys(args, this.dbManager);
|
|
109
|
+
|
|
110
|
+
case 'get_indexes':
|
|
111
|
+
return await handleGetIndexes(args, this.dbManager);
|
|
112
|
+
|
|
113
|
+
case 'information_schema_query':
|
|
114
|
+
return await handleInformationSchemaQuery(args, this.dbManager);
|
|
115
|
+
|
|
116
|
+
case 'execute_query':
|
|
117
|
+
return await handleExecuteQuery(args, this.dbManager);
|
|
118
|
+
|
|
119
|
+
case 'analyze_query':
|
|
120
|
+
return await handleAnalyzeQuery(args, this.dbManager);
|
|
121
|
+
|
|
122
|
+
default:
|
|
123
|
+
Logger.warn(`Unknown tool requested: ${name}`);
|
|
124
|
+
throw new ToolError(`Unknown tool: ${name}`);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
Logger.error(`Error executing tool ${name}:`, error);
|
|
128
|
+
|
|
129
|
+
const errorResponse = createErrorResponse(error instanceof Error ? error : new Error('Unknown error'));
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
content: [{
|
|
133
|
+
type: 'text',
|
|
134
|
+
text: `Error: ${errorResponse.error}`
|
|
135
|
+
}],
|
|
136
|
+
isError: true
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Handle server errors
|
|
142
|
+
this.server.onerror = (error) => {
|
|
143
|
+
Logger.error('Server error:', error);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async initialize(connectionUrls: string[]): Promise<void> {
|
|
148
|
+
Logger.info('Initializing MySQL Inspector Server');
|
|
149
|
+
Logger.info(`Connection URLs provided: ${connectionUrls.length}`);
|
|
150
|
+
|
|
151
|
+
if (connectionUrls.length === 0) {
|
|
152
|
+
throw new Error('At least one database connection URL is required');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Add databases to manager
|
|
156
|
+
for (let i = 0; i < connectionUrls.length; i++) {
|
|
157
|
+
const url = connectionUrls[i];
|
|
158
|
+
try {
|
|
159
|
+
Logger.info(`Adding database connection ${i + 1}/${connectionUrls.length}`);
|
|
160
|
+
const dbName = await this.dbManager.addDatabase(url);
|
|
161
|
+
Logger.info(`Successfully added database: ${dbName}`);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
Logger.error(`Failed to add database from URL: ${url}`, error);
|
|
164
|
+
// Continue with other databases instead of failing completely
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const databases = this.dbManager.listDatabases();
|
|
169
|
+
if (databases.length === 0) {
|
|
170
|
+
throw new Error('No valid database connections could be established');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
Logger.info(`Server initialized with ${databases.length} database(s): ${databases.map(db => db.name).join(', ')}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async run(): Promise<void> {
|
|
177
|
+
Logger.info('Starting MySQL Inspector MCP Server');
|
|
178
|
+
|
|
179
|
+
const transport = new StdioServerTransport();
|
|
180
|
+
await this.server.connect(transport);
|
|
181
|
+
|
|
182
|
+
Logger.info('Server started and listening for requests');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async shutdown(): Promise<void> {
|
|
186
|
+
Logger.info('Shutting down MySQL Inspector Server');
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await this.dbManager.cleanup();
|
|
190
|
+
Logger.info('Database connections cleaned up');
|
|
191
|
+
} catch (error) {
|
|
192
|
+
Logger.error('Error during database cleanup:', error);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
Logger.info('Server shutdown complete');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Utility methods for external use
|
|
199
|
+
getDatabaseManager(): DatabaseManager {
|
|
200
|
+
return this.dbManager;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async getServerInfo(): Promise<any> {
|
|
204
|
+
const databases = this.dbManager.listDatabases();
|
|
205
|
+
return {
|
|
206
|
+
serverName: 'mcp-database-inspector',
|
|
207
|
+
version: '2.0.0',
|
|
208
|
+
connectedDatabases: databases.length,
|
|
209
|
+
databases: databases.map(db => ({
|
|
210
|
+
name: db.name,
|
|
211
|
+
host: db.host,
|
|
212
|
+
database: db.database,
|
|
213
|
+
connected: db.connected,
|
|
214
|
+
lastUsed: db.lastUsed.toISOString()
|
|
215
|
+
})),
|
|
216
|
+
capabilities: [
|
|
217
|
+
'list_databases',
|
|
218
|
+
'list_tables',
|
|
219
|
+
'inspect_table',
|
|
220
|
+
'get_foreign_keys',
|
|
221
|
+
'get_foreign_keys',
|
|
222
|
+
'get_indexes',
|
|
223
|
+
'information_schema_query',
|
|
224
|
+
'execute_query',
|
|
225
|
+
'analyze_query'
|
|
226
|
+
],
|
|
227
|
+
status: databases.length > 0 ? 'ready' : 'no_connections'
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Error handling for uncaught exceptions
|
|
233
|
+
process.on('uncaughtException', (error) => {
|
|
234
|
+
Logger.error('Uncaught exception:', error);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
239
|
+
Logger.error('Unhandled rejection:', { promise, reason });
|
|
240
|
+
process.exit(1);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Graceful shutdown
|
|
244
|
+
process.on('SIGINT', async () => {
|
|
245
|
+
Logger.info('Received SIGINT, shutting down gracefully...');
|
|
246
|
+
process.exit(0);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
process.on('SIGTERM', async () => {
|
|
250
|
+
Logger.info('Received SIGTERM, shutting down gracefully...');
|
|
251
|
+
process.exit(0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
export default DatabaseInspectorServer;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DatabaseManager } from './database/manager.js';
|
|
2
|
+
import { Logger } from './utils/logger.js';
|
|
3
|
+
|
|
4
|
+
async function testConnections() {
|
|
5
|
+
const dbManager = new DatabaseManager();
|
|
6
|
+
|
|
7
|
+
// Default credentials
|
|
8
|
+
const mysqlUrl = 'mysql://root:root@localhost:3306/mysql';
|
|
9
|
+
const postgresUrl = 'postgresql://postgres:postgres@localhost:5432/postgres';
|
|
10
|
+
|
|
11
|
+
Logger.setLogLevel('info');
|
|
12
|
+
console.log('Starting connectivity test with default credentials...');
|
|
13
|
+
|
|
14
|
+
// Test MySQL
|
|
15
|
+
console.log('\n--- Testing MySQL (root/root) ---');
|
|
16
|
+
try {
|
|
17
|
+
const mysqlName = await dbManager.addDatabase(mysqlUrl, 'mysql_default');
|
|
18
|
+
console.log(`✅ Successfully added MySQL database: ${mysqlName}`);
|
|
19
|
+
|
|
20
|
+
const tables = await dbManager.getTables(mysqlName);
|
|
21
|
+
console.log(`✅ Retrieved ${tables.length} tables from MySQL`);
|
|
22
|
+
if (tables.length > 0) {
|
|
23
|
+
const table = tables[0].tableName;
|
|
24
|
+
console.log(`\n--- Analyzing MySQL Query on ${table} ---`);
|
|
25
|
+
const analysis = await dbManager.analyzeQuery(mysqlName, `SELECT * FROM ${table} LIMIT 10`);
|
|
26
|
+
console.log('✅ Analysis Success');
|
|
27
|
+
console.log(' Cost:', analysis.summary.cost);
|
|
28
|
+
console.log(' Operations:', analysis.summary.operations.join(', '));
|
|
29
|
+
if (analysis.summary.potentialIssues.length > 0) {
|
|
30
|
+
console.log(' ⚠️ Issues:', analysis.summary.potentialIssues.join(', '));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`❌ MySQL test failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Test PostgreSQL
|
|
38
|
+
console.log('\n--- Testing PostgreSQL (postgres/postgres) ---');
|
|
39
|
+
try {
|
|
40
|
+
const pgName = await dbManager.addDatabase(postgresUrl, 'postgres_default');
|
|
41
|
+
console.log(`✅ Successfully added PostgreSQL database: ${pgName}`);
|
|
42
|
+
|
|
43
|
+
console.log(`\n--- Analyzing PostgreSQL Query ---`);
|
|
44
|
+
// Use a system table that always exists
|
|
45
|
+
const analysis = await dbManager.analyzeQuery(pgName, 'SELECT * FROM pg_catalog.pg_class LIMIT 5');
|
|
46
|
+
console.log('✅ Analysis Success');
|
|
47
|
+
console.log(' Cost:', analysis.summary.cost);
|
|
48
|
+
console.log(' Operations:', analysis.summary.operations.join(', '));
|
|
49
|
+
if (analysis.summary.potentialIssues.length > 0) {
|
|
50
|
+
console.log(' ⚠️ Issues:', analysis.summary.potentialIssues.join(', '));
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(`❌ PostgreSQL test failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await dbManager.cleanup();
|
|
57
|
+
console.log('\nTest complete.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
testConnections().catch(err => {
|
|
61
|
+
console.error('Fatal error during test:', err);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|