mcp-db-analyzer 0.2.0 → 0.2.3
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/build/db-mysql.js +4 -1
- package/build/db-postgres.js +9 -1
- package/build/db.js +7 -0
- package/build/index.js +38 -59
- package/package.json +11 -5
- package/build/license.js +0 -114
package/build/db-mysql.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import mysql from "mysql2/promise";
|
|
2
|
+
import { getConnectionTimeoutMs } from "./db.js";
|
|
2
3
|
function wrapConnectionError(err) {
|
|
3
4
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4
5
|
const sanitized = msg.replace(/\/\/[^@]+@/g, "//****:****@");
|
|
@@ -11,9 +12,10 @@ export function createMysqlAdapter() {
|
|
|
11
12
|
let pool = null;
|
|
12
13
|
function getPool() {
|
|
13
14
|
if (!pool) {
|
|
15
|
+
const timeoutMs = getConnectionTimeoutMs();
|
|
14
16
|
const uri = process.env.DATABASE_URL;
|
|
15
17
|
if (uri) {
|
|
16
|
-
pool = mysql.createPool(uri);
|
|
18
|
+
pool = mysql.createPool({ uri, connectTimeout: timeoutMs });
|
|
17
19
|
}
|
|
18
20
|
else {
|
|
19
21
|
pool = mysql.createPool({
|
|
@@ -22,6 +24,7 @@ export function createMysqlAdapter() {
|
|
|
22
24
|
database: process.env.MYSQL_DATABASE || process.env.DB_NAME,
|
|
23
25
|
user: process.env.MYSQL_USER || process.env.DB_USER,
|
|
24
26
|
password: process.env.MYSQL_PASSWORD || process.env.DB_PASSWORD,
|
|
27
|
+
connectTimeout: timeoutMs,
|
|
25
28
|
});
|
|
26
29
|
}
|
|
27
30
|
}
|
package/build/db-postgres.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
+
import { getConnectionTimeoutMs } from "./db.js";
|
|
2
3
|
const { Pool } = pg;
|
|
3
4
|
function wrapConnectionError(err) {
|
|
4
5
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -12,9 +13,14 @@ export function createPostgresAdapter() {
|
|
|
12
13
|
let pool = null;
|
|
13
14
|
function getPool() {
|
|
14
15
|
if (!pool) {
|
|
16
|
+
const timeoutMs = getConnectionTimeoutMs();
|
|
15
17
|
const connectionString = process.env.DATABASE_URL;
|
|
16
18
|
if (connectionString) {
|
|
17
|
-
pool = new Pool({
|
|
19
|
+
pool = new Pool({
|
|
20
|
+
connectionString,
|
|
21
|
+
connectionTimeoutMillis: timeoutMs,
|
|
22
|
+
query_timeout: timeoutMs,
|
|
23
|
+
});
|
|
18
24
|
}
|
|
19
25
|
else {
|
|
20
26
|
pool = new Pool({
|
|
@@ -23,6 +29,8 @@ export function createPostgresAdapter() {
|
|
|
23
29
|
database: process.env.PGDATABASE || "postgres",
|
|
24
30
|
user: process.env.PGUSER,
|
|
25
31
|
password: process.env.PGPASSWORD,
|
|
32
|
+
connectionTimeoutMillis: timeoutMs,
|
|
33
|
+
query_timeout: timeoutMs,
|
|
26
34
|
});
|
|
27
35
|
}
|
|
28
36
|
}
|
package/build/db.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
let adapter = null;
|
|
2
2
|
let driverType = "postgres";
|
|
3
|
+
let connectionTimeoutMs = 30000;
|
|
3
4
|
export function getDriverType() {
|
|
4
5
|
return driverType;
|
|
5
6
|
}
|
|
7
|
+
export function getConnectionTimeoutMs() {
|
|
8
|
+
return connectionTimeoutMs;
|
|
9
|
+
}
|
|
10
|
+
export function setConnectionTimeoutMs(ms) {
|
|
11
|
+
connectionTimeoutMs = ms;
|
|
12
|
+
}
|
|
6
13
|
export function setAdapter(a) {
|
|
7
14
|
adapter = a;
|
|
8
15
|
driverType = a.driver;
|
package/build/index.js
CHANGED
|
@@ -11,11 +11,8 @@ import { analyzeSlowQueries } from "./analyzers/slow-queries.js";
|
|
|
11
11
|
import { analyzeConnections } from "./analyzers/connections.js";
|
|
12
12
|
import { analyzeTableRelationships } from "./analyzers/relationships.js";
|
|
13
13
|
import { analyzeVacuum } from "./analyzers/vacuum.js";
|
|
14
|
-
import { closePool, initDriver } from "./db.js";
|
|
14
|
+
import { closePool, initDriver, setConnectionTimeoutMs } from "./db.js";
|
|
15
15
|
import { formatToolError } from "./errors.js";
|
|
16
|
-
import { validateLicense, formatUpgradePrompt } from "./license.js";
|
|
17
|
-
// License check (reads MCP_LICENSE_KEY env var once at startup)
|
|
18
|
-
const license = validateLicense(process.env.MCP_LICENSE_KEY, "db-analyzer");
|
|
19
16
|
// Handle --help
|
|
20
17
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
21
18
|
console.log(`mcp-db-analyzer v0.1.0 — MCP server for database analysis
|
|
@@ -73,6 +70,15 @@ const server = new McpServer({
|
|
|
73
70
|
name: "mcp-db-analyzer",
|
|
74
71
|
version: "0.1.0",
|
|
75
72
|
});
|
|
73
|
+
// Shared Zod parameter for connection timeout
|
|
74
|
+
const timeoutParam = z
|
|
75
|
+
.number()
|
|
76
|
+
.optional()
|
|
77
|
+
.default(30000)
|
|
78
|
+
.describe("Connection timeout in milliseconds (default: 30000). Increase for slow or remote databases.");
|
|
79
|
+
function applyTimeout(timeout_ms) {
|
|
80
|
+
setConnectionTimeoutMs(timeout_ms);
|
|
81
|
+
}
|
|
76
82
|
// --- Tool: inspect_schema ---
|
|
77
83
|
server.tool("inspect_schema", "List all tables in a schema with row counts and sizes, or inspect a specific table's columns, types, constraints, and foreign keys.", {
|
|
78
84
|
table: z
|
|
@@ -83,7 +89,9 @@ server.tool("inspect_schema", "List all tables in a schema with row counts and s
|
|
|
83
89
|
.string()
|
|
84
90
|
.default("public")
|
|
85
91
|
.describe("Database schema to inspect (default: public)"),
|
|
86
|
-
|
|
92
|
+
timeout_ms: timeoutParam,
|
|
93
|
+
}, async ({ table, schema, timeout_ms }) => {
|
|
94
|
+
applyTimeout(timeout_ms);
|
|
87
95
|
try {
|
|
88
96
|
const result = table
|
|
89
97
|
? await inspectTable(table, schema)
|
|
@@ -111,7 +119,9 @@ server.tool("analyze_indexes", "Analyze index usage statistics to find unused in
|
|
|
111
119
|
.enum(["usage", "missing", "all"])
|
|
112
120
|
.default("all")
|
|
113
121
|
.describe("Analysis mode: 'usage' for unused index detection, 'missing' for missing index suggestions, 'all' for both"),
|
|
114
|
-
|
|
122
|
+
timeout_ms: timeoutParam,
|
|
123
|
+
}, async ({ schema, mode, timeout_ms }) => {
|
|
124
|
+
applyTimeout(timeout_ms);
|
|
115
125
|
try {
|
|
116
126
|
const parts = [];
|
|
117
127
|
if (mode === "usage" || mode === "all") {
|
|
@@ -140,7 +150,9 @@ server.tool("explain_query", "Run EXPLAIN on a SQL query and return a formatted
|
|
|
140
150
|
.boolean()
|
|
141
151
|
.default(false)
|
|
142
152
|
.describe("Run EXPLAIN ANALYZE to get actual execution times (executes the query). Only allowed for SELECT queries."),
|
|
143
|
-
|
|
153
|
+
timeout_ms: timeoutParam,
|
|
154
|
+
}, async ({ sql, analyze, timeout_ms }) => {
|
|
155
|
+
applyTimeout(timeout_ms);
|
|
144
156
|
try {
|
|
145
157
|
const result = await explainQuery(sql, analyze);
|
|
146
158
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -162,7 +174,9 @@ server.tool("analyze_table_bloat", "Analyze table bloat by checking dead tuple r
|
|
|
162
174
|
.string()
|
|
163
175
|
.default("public")
|
|
164
176
|
.describe("Database schema to analyze (default: public)"),
|
|
165
|
-
|
|
177
|
+
timeout_ms: timeoutParam,
|
|
178
|
+
}, async ({ schema, timeout_ms }) => {
|
|
179
|
+
applyTimeout(timeout_ms);
|
|
166
180
|
try {
|
|
167
181
|
const result = await analyzeTableBloat(schema);
|
|
168
182
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -184,18 +198,9 @@ server.tool("suggest_missing_indexes", "Find tables with high sequential scan co
|
|
|
184
198
|
.string()
|
|
185
199
|
.default("public")
|
|
186
200
|
.describe("Database schema to analyze (default: public)"),
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
content: [{
|
|
191
|
-
type: "text",
|
|
192
|
-
text: formatUpgradePrompt("suggest_missing_indexes", "Actionable index recommendations with:\n" +
|
|
193
|
-
"- Tables with high sequential scan counts\n" +
|
|
194
|
-
"- Cross-referenced unused indexes wasting space\n" +
|
|
195
|
-
"- Ready-to-run CREATE INDEX and DROP INDEX statements"),
|
|
196
|
-
}],
|
|
197
|
-
};
|
|
198
|
-
}
|
|
201
|
+
timeout_ms: timeoutParam,
|
|
202
|
+
}, async ({ schema, timeout_ms }) => {
|
|
203
|
+
applyTimeout(timeout_ms);
|
|
199
204
|
try {
|
|
200
205
|
const result = await suggestMissingIndexes(schema);
|
|
201
206
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -221,18 +226,9 @@ server.tool("analyze_slow_queries", "Find the slowest queries using pg_stat_stat
|
|
|
221
226
|
.number()
|
|
222
227
|
.default(10)
|
|
223
228
|
.describe("Number of slow queries to return (default: 10)"),
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
content: [{
|
|
228
|
-
type: "text",
|
|
229
|
-
text: formatUpgradePrompt("analyze_slow_queries", "Slow query analysis with:\n" +
|
|
230
|
-
"- Top queries ranked by total execution time\n" +
|
|
231
|
-
"- Call counts, mean/max times, rows returned\n" +
|
|
232
|
-
"- Optimization recommendations"),
|
|
233
|
-
}],
|
|
234
|
-
};
|
|
235
|
-
}
|
|
229
|
+
timeout_ms: timeoutParam,
|
|
230
|
+
}, async ({ schema, limit, timeout_ms }) => {
|
|
231
|
+
applyTimeout(timeout_ms);
|
|
236
232
|
try {
|
|
237
233
|
const result = await analyzeSlowQueries(schema, limit);
|
|
238
234
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -249,19 +245,10 @@ server.tool("analyze_slow_queries", "Find the slowest queries using pg_stat_stat
|
|
|
249
245
|
}
|
|
250
246
|
});
|
|
251
247
|
// Tool 7: analyze_connections
|
|
252
|
-
server.tool("analyze_connections", "Analyze active database connections. Detects idle-in-transaction sessions, long-running queries, lock contention, and connection pool utilization. PostgreSQL and MySQL only.", {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
type: "text",
|
|
257
|
-
text: formatUpgradePrompt("analyze_connections", "Connection analysis with:\n" +
|
|
258
|
-
"- Idle-in-transaction session detection\n" +
|
|
259
|
-
"- Long-running query identification\n" +
|
|
260
|
-
"- Lock contention analysis\n" +
|
|
261
|
-
"- Connection pool utilization metrics"),
|
|
262
|
-
}],
|
|
263
|
-
};
|
|
264
|
-
}
|
|
248
|
+
server.tool("analyze_connections", "Analyze active database connections. Detects idle-in-transaction sessions, long-running queries, lock contention, and connection pool utilization. PostgreSQL and MySQL only.", {
|
|
249
|
+
timeout_ms: timeoutParam,
|
|
250
|
+
}, async ({ timeout_ms }) => {
|
|
251
|
+
applyTimeout(timeout_ms);
|
|
265
252
|
try {
|
|
266
253
|
const result = await analyzeConnections();
|
|
267
254
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -283,19 +270,9 @@ server.tool("analyze_table_relationships", "Analyze foreign key relationships be
|
|
|
283
270
|
.string()
|
|
284
271
|
.default("public")
|
|
285
272
|
.describe("Database schema to analyze (default: public)"),
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
content: [{
|
|
290
|
-
type: "text",
|
|
291
|
-
text: formatUpgradePrompt("analyze_table_relationships", "Table relationship analysis with:\n" +
|
|
292
|
-
"- Foreign key dependency graph\n" +
|
|
293
|
-
"- Orphan table detection\n" +
|
|
294
|
-
"- Cascading delete chain analysis\n" +
|
|
295
|
-
"- Hub entity identification"),
|
|
296
|
-
}],
|
|
297
|
-
};
|
|
298
|
-
}
|
|
273
|
+
timeout_ms: timeoutParam,
|
|
274
|
+
}, async ({ schema, timeout_ms }) => {
|
|
275
|
+
applyTimeout(timeout_ms);
|
|
299
276
|
try {
|
|
300
277
|
const result = await analyzeTableRelationships(schema);
|
|
301
278
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -317,7 +294,9 @@ server.tool("analyze_vacuum", "Analyze PostgreSQL VACUUM maintenance status. Che
|
|
|
317
294
|
.string()
|
|
318
295
|
.default("public")
|
|
319
296
|
.describe("Database schema to analyze (default: public)"),
|
|
320
|
-
|
|
297
|
+
timeout_ms: timeoutParam,
|
|
298
|
+
}, async ({ schema, timeout_ms }) => {
|
|
299
|
+
applyTimeout(timeout_ms);
|
|
321
300
|
try {
|
|
322
301
|
const result = await analyzeVacuum(schema);
|
|
323
302
|
return { content: [{ type: "text", text: result }] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-db-analyzer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "MCP server for PostgreSQL, MySQL, and SQLite schema analysis, index optimization, and query plan inspection",
|
|
5
5
|
"author": "Dmytro Lisnichenko",
|
|
6
6
|
"type": "module",
|
|
@@ -24,16 +24,22 @@
|
|
|
24
24
|
],
|
|
25
25
|
"keywords": [
|
|
26
26
|
"mcp",
|
|
27
|
+
"mcp-server",
|
|
27
28
|
"model-context-protocol",
|
|
29
|
+
"ai",
|
|
30
|
+
"claude",
|
|
31
|
+
"anthropic",
|
|
32
|
+
"database",
|
|
33
|
+
"sql",
|
|
34
|
+
"schema",
|
|
35
|
+
"database-analyzer",
|
|
36
|
+
"database-diagnostics",
|
|
28
37
|
"postgresql",
|
|
29
38
|
"mysql",
|
|
30
39
|
"sqlite",
|
|
31
|
-
"database",
|
|
32
40
|
"schema-analyzer",
|
|
33
41
|
"index-optimization",
|
|
34
|
-
"query-plan"
|
|
35
|
-
"ai",
|
|
36
|
-
"claude"
|
|
42
|
+
"query-plan"
|
|
37
43
|
],
|
|
38
44
|
"license": "MIT",
|
|
39
45
|
"engines": {
|
package/build/license.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* License validation for MCP Migration Advisor (Pro features).
|
|
3
|
-
*
|
|
4
|
-
* Validates license keys offline using HMAC-SHA256.
|
|
5
|
-
* Missing or invalid keys gracefully degrade to free mode — never errors.
|
|
6
|
-
*
|
|
7
|
-
* Key format: MCPJBS-XXXXX-XXXXX-XXXXX-XXXXX
|
|
8
|
-
* Payload (12 bytes = 20 base32 chars):
|
|
9
|
-
* [0] product mask (8 bits)
|
|
10
|
-
* [1-2] expiry days since 2026-01-01 (16 bits)
|
|
11
|
-
* [3-5] customer ID (24 bits)
|
|
12
|
-
* [6-11] HMAC-SHA256 truncated (48 bits)
|
|
13
|
-
*/
|
|
14
|
-
import { createHmac } from "node:crypto";
|
|
15
|
-
const KEY_PREFIX = "MCPJBS-";
|
|
16
|
-
const EPOCH = new Date("2026-01-01T00:00:00Z");
|
|
17
|
-
const BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
18
|
-
const HMAC_SECRET = "mcp-java-backend-suite-license-v1";
|
|
19
|
-
const PRODUCTS = {
|
|
20
|
-
"db-analyzer": 0,
|
|
21
|
-
"jvm-diagnostics": 1,
|
|
22
|
-
"migration-advisor": 2,
|
|
23
|
-
"spring-boot-actuator": 3,
|
|
24
|
-
"redis-diagnostics": 4,
|
|
25
|
-
};
|
|
26
|
-
export function validateLicense(key, product) {
|
|
27
|
-
const FREE = {
|
|
28
|
-
isPro: false,
|
|
29
|
-
expiresAt: null,
|
|
30
|
-
customerId: null,
|
|
31
|
-
reason: "No license key provided",
|
|
32
|
-
};
|
|
33
|
-
if (!key || key.trim().length === 0)
|
|
34
|
-
return FREE;
|
|
35
|
-
const trimmed = key.trim().toUpperCase();
|
|
36
|
-
if (!trimmed.startsWith(KEY_PREFIX)) {
|
|
37
|
-
return { ...FREE, reason: "Invalid key format: missing MCPJBS- prefix" };
|
|
38
|
-
}
|
|
39
|
-
const body = trimmed.slice(KEY_PREFIX.length).replace(/-/g, "");
|
|
40
|
-
if (body.length < 20) {
|
|
41
|
-
return { ...FREE, reason: "Invalid key format: too short" };
|
|
42
|
-
}
|
|
43
|
-
let decoded;
|
|
44
|
-
try {
|
|
45
|
-
decoded = base32Decode(body.slice(0, 20));
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return { ...FREE, reason: "Invalid key format: bad base32 encoding" };
|
|
49
|
-
}
|
|
50
|
-
if (decoded.length < 12) {
|
|
51
|
-
return { ...FREE, reason: "Invalid key format: decoded data too short" };
|
|
52
|
-
}
|
|
53
|
-
const payload = decoded.subarray(0, 6);
|
|
54
|
-
const providedSignature = decoded.subarray(6, 12);
|
|
55
|
-
const expectedHmac = createHmac("sha256", HMAC_SECRET)
|
|
56
|
-
.update(payload)
|
|
57
|
-
.digest();
|
|
58
|
-
const expectedSignature = expectedHmac.subarray(0, 6);
|
|
59
|
-
if (!providedSignature.equals(expectedSignature)) {
|
|
60
|
-
return { ...FREE, reason: "Invalid license key: signature mismatch" };
|
|
61
|
-
}
|
|
62
|
-
const productMask = payload[0];
|
|
63
|
-
const daysSinceEpoch = (payload[1] << 8) | payload[2];
|
|
64
|
-
const customerId = (payload[3] << 16) | (payload[4] << 8) | payload[5];
|
|
65
|
-
const productBit = PRODUCTS[product];
|
|
66
|
-
if (productBit === undefined) {
|
|
67
|
-
return { ...FREE, reason: `Unknown product: ${product}` };
|
|
68
|
-
}
|
|
69
|
-
if ((productMask & (1 << productBit)) === 0) {
|
|
70
|
-
return { ...FREE, customerId, reason: `License does not include ${product}` };
|
|
71
|
-
}
|
|
72
|
-
const expiresAt = new Date(EPOCH.getTime() + daysSinceEpoch * 24 * 60 * 60 * 1000);
|
|
73
|
-
if (new Date() > expiresAt) {
|
|
74
|
-
return {
|
|
75
|
-
isPro: false,
|
|
76
|
-
expiresAt,
|
|
77
|
-
customerId,
|
|
78
|
-
reason: `License expired on ${expiresAt.toISOString().slice(0, 10)}`,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
return { isPro: true, expiresAt, customerId, reason: "Valid Pro license" };
|
|
82
|
-
}
|
|
83
|
-
export function formatUpgradePrompt(toolName, featureDescription) {
|
|
84
|
-
return [
|
|
85
|
-
`## ${toolName} (Pro Feature)`,
|
|
86
|
-
"",
|
|
87
|
-
"This analysis is available with MCP Java Backend Suite Pro.",
|
|
88
|
-
"",
|
|
89
|
-
`**What you'll get:**`,
|
|
90
|
-
featureDescription,
|
|
91
|
-
"",
|
|
92
|
-
"**Upgrade**: https://mcpjbs.dev/pricing",
|
|
93
|
-
"**Price**: $19/month or $190/year",
|
|
94
|
-
"",
|
|
95
|
-
"> Already have a key? Set `MCP_LICENSE_KEY` in your Claude Desktop config.",
|
|
96
|
-
].join("\n");
|
|
97
|
-
}
|
|
98
|
-
function base32Decode(encoded) {
|
|
99
|
-
const bytes = [];
|
|
100
|
-
let bits = 0;
|
|
101
|
-
let value = 0;
|
|
102
|
-
for (const char of encoded) {
|
|
103
|
-
const idx = BASE32_CHARS.indexOf(char);
|
|
104
|
-
if (idx === -1)
|
|
105
|
-
throw new Error(`Invalid base32 character: ${char}`);
|
|
106
|
-
value = (value << 5) | idx;
|
|
107
|
-
bits += 5;
|
|
108
|
-
if (bits >= 8) {
|
|
109
|
-
bits -= 8;
|
|
110
|
-
bytes.push((value >> bits) & 0xff);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return Buffer.from(bytes);
|
|
114
|
-
}
|