@adevguide/mcp-database-server 1.0.2 → 1.0.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/README.md +1180 -1049
- package/dist/index.js +658 -23
- package/dist/index.js.map +1 -1
- package/mcp-database-server.config.example +59 -59
- package/package.json +78 -79
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import dotenv from "dotenv";
|
|
5
5
|
import { parseArgs } from "util";
|
|
6
|
-
import { readFileSync } from "fs";
|
|
6
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { dirname as dirname2, join as join2 } from "path";
|
|
9
9
|
|
|
@@ -264,7 +264,34 @@ function findJoinPaths(tables, relationships, maxDepth = 3) {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
// src/config.ts
|
|
267
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
268
|
+
const projectMarkers = ["package.json", ".git", "tsconfig.json", "pyproject.toml", "Cargo.toml", "go.mod"];
|
|
269
|
+
let currentDir = resolve(startDir);
|
|
270
|
+
while (true) {
|
|
271
|
+
for (const marker of projectMarkers) {
|
|
272
|
+
if (existsSync(join(currentDir, marker))) {
|
|
273
|
+
return currentDir;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const parentDir = dirname(currentDir);
|
|
277
|
+
if (parentDir === currentDir) {
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
currentDir = parentDir;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
267
284
|
function findConfigFile(fileName, startDir = process.cwd()) {
|
|
285
|
+
const projectRoot = findProjectRoot(startDir);
|
|
286
|
+
if (projectRoot) {
|
|
287
|
+
const configFromProjectRoot = findConfigFileFromDir(fileName, projectRoot);
|
|
288
|
+
if (configFromProjectRoot) {
|
|
289
|
+
return configFromProjectRoot;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return findConfigFileFromDir(fileName, startDir);
|
|
293
|
+
}
|
|
294
|
+
function findConfigFileFromDir(fileName, startDir) {
|
|
268
295
|
let currentDir = resolve(startDir);
|
|
269
296
|
while (true) {
|
|
270
297
|
const configPath = join(currentDir, fileName);
|
|
@@ -336,17 +363,21 @@ import pg from "pg";
|
|
|
336
363
|
import pino from "pino";
|
|
337
364
|
var logger;
|
|
338
365
|
function initLogger(level = "info", pretty = false) {
|
|
339
|
-
logger = pino(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
366
|
+
logger = pino(
|
|
367
|
+
{
|
|
368
|
+
level,
|
|
369
|
+
transport: pretty ? {
|
|
370
|
+
target: "pino-pretty",
|
|
371
|
+
options: {
|
|
372
|
+
colorize: true,
|
|
373
|
+
translateTime: "SYS:standard",
|
|
374
|
+
ignore: "pid,hostname"
|
|
375
|
+
}
|
|
376
|
+
} : void 0
|
|
377
|
+
},
|
|
378
|
+
pino.destination({ dest: 2, sync: false })
|
|
379
|
+
// Write to stderr (fd 2) for MCP protocol compatibility
|
|
380
|
+
);
|
|
350
381
|
return logger;
|
|
351
382
|
}
|
|
352
383
|
function getLogger() {
|
|
@@ -1607,18 +1638,370 @@ var SchemaCache = class {
|
|
|
1607
1638
|
}
|
|
1608
1639
|
};
|
|
1609
1640
|
|
|
1641
|
+
// src/query-optimizer.ts
|
|
1642
|
+
var QueryOptimizer = class {
|
|
1643
|
+
slowQueryThresholdMs;
|
|
1644
|
+
slowQueryAlerts = /* @__PURE__ */ new Map();
|
|
1645
|
+
constructor(options = {
|
|
1646
|
+
slowQueryThresholdMs: 1e3,
|
|
1647
|
+
maxHistoryForAnalysis: 1e3,
|
|
1648
|
+
enableAutoAnalysis: true
|
|
1649
|
+
}) {
|
|
1650
|
+
this.slowQueryThresholdMs = options.slowQueryThresholdMs;
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Analyze query complexity and extract performance metrics
|
|
1654
|
+
*/
|
|
1655
|
+
analyzeQueryComplexity(sql) {
|
|
1656
|
+
const complexity = {
|
|
1657
|
+
selectColumns: this.countSelectColumns(sql),
|
|
1658
|
+
whereConditions: this.countWhereConditions(sql),
|
|
1659
|
+
joinCount: this.countJoins(sql),
|
|
1660
|
+
subqueryCount: this.countSubqueries(sql),
|
|
1661
|
+
hasAggregations: /\b(COUNT|SUM|AVG|MIN|MAX)\s*\(/i.test(sql),
|
|
1662
|
+
hasDistinct: /\bDISTINCT\b/i.test(sql),
|
|
1663
|
+
hasOrderBy: /\bORDER\s+BY\b/i.test(sql),
|
|
1664
|
+
hasGroupBy: /\bGROUP\s+BY\b/i.test(sql),
|
|
1665
|
+
estimatedComplexity: "simple"
|
|
1666
|
+
};
|
|
1667
|
+
let score = 0;
|
|
1668
|
+
score += complexity.selectColumns * 0.5;
|
|
1669
|
+
score += complexity.whereConditions * 1;
|
|
1670
|
+
score += complexity.joinCount * 2;
|
|
1671
|
+
score += complexity.subqueryCount * 3;
|
|
1672
|
+
score += complexity.hasAggregations ? 2 : 0;
|
|
1673
|
+
score += complexity.hasDistinct ? 1 : 0;
|
|
1674
|
+
score += complexity.hasOrderBy ? 1 : 0;
|
|
1675
|
+
score += complexity.hasGroupBy ? 1 : 0;
|
|
1676
|
+
if (score <= 3) complexity.estimatedComplexity = "simple";
|
|
1677
|
+
else if (score <= 7) complexity.estimatedComplexity = "medium";
|
|
1678
|
+
else if (score <= 12) complexity.estimatedComplexity = "complex";
|
|
1679
|
+
else complexity.estimatedComplexity = "very_complex";
|
|
1680
|
+
return complexity;
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Generate index recommendations based on query history and schema
|
|
1684
|
+
*/
|
|
1685
|
+
generateIndexRecommendations(queryHistory, schema) {
|
|
1686
|
+
const recommendations = [];
|
|
1687
|
+
const columnUsage = /* @__PURE__ */ new Map();
|
|
1688
|
+
for (const entry of queryHistory) {
|
|
1689
|
+
if (entry.error) continue;
|
|
1690
|
+
const tables = entry.tables;
|
|
1691
|
+
const whereMatch = entry.sql.match(/WHERE\s+(.+?)(?:\s+(GROUP|ORDER|LIMIT|$))/i);
|
|
1692
|
+
if (whereMatch) {
|
|
1693
|
+
const whereClause = whereMatch[1];
|
|
1694
|
+
const columns = this.extractColumnsFromCondition(whereClause, tables, schema);
|
|
1695
|
+
for (const col of columns) {
|
|
1696
|
+
const key = `${col.table}.${col.column}`;
|
|
1697
|
+
const existing = columnUsage.get(key) || { table: col.table, usage: 0, inWhere: false, inJoin: false };
|
|
1698
|
+
existing.usage++;
|
|
1699
|
+
existing.inWhere = true;
|
|
1700
|
+
columnUsage.set(key, existing);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
const joinMatches = entry.sql.match(/JOIN\s+\w+\s+ON\s+(.+?)(?:\s+(WHERE|GROUP|ORDER|LIMIT|$))/gi);
|
|
1704
|
+
if (joinMatches) {
|
|
1705
|
+
for (const joinMatch of joinMatches) {
|
|
1706
|
+
const joinClause = joinMatch.replace(/JOIN\s+\w+\s+ON\s+/i, "");
|
|
1707
|
+
const columns = this.extractColumnsFromCondition(joinClause, tables, schema);
|
|
1708
|
+
for (const col of columns) {
|
|
1709
|
+
const key = `${col.table}.${col.column}`;
|
|
1710
|
+
const existing = columnUsage.get(key) || { table: col.table, usage: 0, inWhere: false, inJoin: false };
|
|
1711
|
+
existing.usage++;
|
|
1712
|
+
existing.inJoin = true;
|
|
1713
|
+
columnUsage.set(key, existing);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
for (const [key, usage] of columnUsage) {
|
|
1719
|
+
if (usage.usage < 3) continue;
|
|
1720
|
+
const [tableName, columnName] = key.split(".");
|
|
1721
|
+
const table = this.findTableInSchema(schema, tableName);
|
|
1722
|
+
if (!table) continue;
|
|
1723
|
+
const existingIndex = table.indexes.find(
|
|
1724
|
+
(idx) => idx.columns.includes(columnName) && !idx.isPrimary
|
|
1725
|
+
);
|
|
1726
|
+
if (existingIndex) continue;
|
|
1727
|
+
const recommendation = {
|
|
1728
|
+
table: tableName,
|
|
1729
|
+
columns: [columnName],
|
|
1730
|
+
type: "single",
|
|
1731
|
+
reason: `Column ${columnName} is frequently used in ${usage.inWhere ? "WHERE" : ""}${usage.inWhere && usage.inJoin ? " and " : ""}${usage.inJoin ? "JOIN" : ""} conditions`,
|
|
1732
|
+
impact: usage.usage > 10 ? "high" : usage.usage > 5 ? "medium" : "low"
|
|
1733
|
+
};
|
|
1734
|
+
recommendations.push(recommendation);
|
|
1735
|
+
}
|
|
1736
|
+
return recommendations.sort((a, b) => {
|
|
1737
|
+
const impactOrder = { high: 3, medium: 2, low: 1 };
|
|
1738
|
+
return impactOrder[b.impact] - impactOrder[a.impact];
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Profile query performance using EXPLAIN plan
|
|
1743
|
+
*/
|
|
1744
|
+
async profileQueryPerformance(_dbId, sql, explainResult, executionTimeMs, rowCount) {
|
|
1745
|
+
const bottlenecks = [];
|
|
1746
|
+
const recommendations = [];
|
|
1747
|
+
if (explainResult.plan) {
|
|
1748
|
+
bottlenecks.push(...this.analyzeExplainPlan(explainResult.plan));
|
|
1749
|
+
}
|
|
1750
|
+
if (executionTimeMs > this.slowQueryThresholdMs) {
|
|
1751
|
+
bottlenecks.push({
|
|
1752
|
+
type: "table_scan",
|
|
1753
|
+
severity: executionTimeMs > this.slowQueryThresholdMs * 5 ? "critical" : "high",
|
|
1754
|
+
description: `Query execution time (${executionTimeMs}ms) exceeds threshold (${this.slowQueryThresholdMs}ms)`,
|
|
1755
|
+
estimatedCost: executionTimeMs
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
for (const bottleneck of bottlenecks) {
|
|
1759
|
+
switch (bottleneck.type) {
|
|
1760
|
+
case "table_scan":
|
|
1761
|
+
recommendations.push({
|
|
1762
|
+
type: "add_index",
|
|
1763
|
+
description: `Consider adding an index on ${bottleneck.table || "frequently queried columns"}`,
|
|
1764
|
+
impact: "high",
|
|
1765
|
+
effort: "medium"
|
|
1766
|
+
});
|
|
1767
|
+
break;
|
|
1768
|
+
case "join":
|
|
1769
|
+
recommendations.push({
|
|
1770
|
+
type: "optimize_join",
|
|
1771
|
+
description: "Review JOIN conditions and ensure proper indexing on join columns",
|
|
1772
|
+
impact: "high",
|
|
1773
|
+
effort: "medium"
|
|
1774
|
+
});
|
|
1775
|
+
break;
|
|
1776
|
+
case "sort":
|
|
1777
|
+
recommendations.push({
|
|
1778
|
+
type: "add_index",
|
|
1779
|
+
description: "Consider adding an index to avoid sorting operations",
|
|
1780
|
+
impact: "medium",
|
|
1781
|
+
effort: "medium"
|
|
1782
|
+
});
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
let score = 100;
|
|
1787
|
+
for (const bottleneck of bottlenecks) {
|
|
1788
|
+
const severityPenalty = { critical: 30, high: 20, medium: 10, low: 5 };
|
|
1789
|
+
score -= severityPenalty[bottleneck.severity];
|
|
1790
|
+
}
|
|
1791
|
+
score = Math.max(0, Math.min(100, score));
|
|
1792
|
+
return {
|
|
1793
|
+
queryId: this.generateQueryId(sql),
|
|
1794
|
+
sql,
|
|
1795
|
+
executionTimeMs,
|
|
1796
|
+
rowCount,
|
|
1797
|
+
bottlenecks,
|
|
1798
|
+
recommendations,
|
|
1799
|
+
overallScore: score
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Detect and alert on slow queries
|
|
1804
|
+
*/
|
|
1805
|
+
detectSlowQueries(queryHistory, dbId) {
|
|
1806
|
+
for (const entry of queryHistory) {
|
|
1807
|
+
if (entry.executionTimeMs > this.slowQueryThresholdMs) {
|
|
1808
|
+
const queryId = this.generateQueryId(entry.sql);
|
|
1809
|
+
const existingAlerts = this.slowQueryAlerts.get(dbId) || [];
|
|
1810
|
+
const existingAlert = existingAlerts.find((a) => a.queryId === queryId);
|
|
1811
|
+
if (existingAlert) {
|
|
1812
|
+
existingAlert.frequency++;
|
|
1813
|
+
existingAlert.timestamp = entry.timestamp;
|
|
1814
|
+
if (entry.executionTimeMs > existingAlert.executionTimeMs) {
|
|
1815
|
+
existingAlert.executionTimeMs = entry.executionTimeMs;
|
|
1816
|
+
}
|
|
1817
|
+
} else {
|
|
1818
|
+
const alert = {
|
|
1819
|
+
dbId,
|
|
1820
|
+
queryId,
|
|
1821
|
+
sql: entry.sql,
|
|
1822
|
+
executionTimeMs: entry.executionTimeMs,
|
|
1823
|
+
thresholdMs: this.slowQueryThresholdMs,
|
|
1824
|
+
timestamp: entry.timestamp,
|
|
1825
|
+
frequency: 1,
|
|
1826
|
+
recommendations: []
|
|
1827
|
+
};
|
|
1828
|
+
existingAlerts.push(alert);
|
|
1829
|
+
}
|
|
1830
|
+
this.slowQueryAlerts.set(dbId, existingAlerts);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return this.slowQueryAlerts.get(dbId) || [];
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Suggest optimized versions of queries
|
|
1837
|
+
*/
|
|
1838
|
+
suggestQueryRewrites(sql, schema) {
|
|
1839
|
+
const optimizations = [];
|
|
1840
|
+
let optimizedQuery = sql;
|
|
1841
|
+
let performanceGain = 0;
|
|
1842
|
+
if (/\bDISTINCT\b/i.test(sql) && this.canRemoveDistinct(sql, schema)) {
|
|
1843
|
+
optimizedQuery = optimizedQuery.replace(/\bDISTINCT\b/i, "");
|
|
1844
|
+
optimizations.push("Removed unnecessary DISTINCT clause");
|
|
1845
|
+
performanceGain += 15;
|
|
1846
|
+
}
|
|
1847
|
+
if (!/\bLIMIT\b/i.test(sql) && !/\bCOUNT\b/i.test(sql)) {
|
|
1848
|
+
optimizedQuery += " LIMIT 1000";
|
|
1849
|
+
optimizations.push("Added LIMIT clause to prevent large result sets");
|
|
1850
|
+
performanceGain += 10;
|
|
1851
|
+
}
|
|
1852
|
+
if (/\bSELECT\s+\*\s+FROM\b/i.test(sql)) {
|
|
1853
|
+
optimizations.push("Consider selecting only required columns instead of SELECT *");
|
|
1854
|
+
performanceGain += 5;
|
|
1855
|
+
}
|
|
1856
|
+
const tables = extractTableNames(sql);
|
|
1857
|
+
for (const table of tables) {
|
|
1858
|
+
const tableMeta = this.findTableInSchema(schema, table);
|
|
1859
|
+
if (tableMeta && !/\bWHERE\b/i.test(sql)) {
|
|
1860
|
+
optimizations.push(`Consider adding WHERE clause for table ${table} to reduce data scanned`);
|
|
1861
|
+
performanceGain += 20;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
return {
|
|
1865
|
+
originalQuery: sql,
|
|
1866
|
+
optimizedQuery,
|
|
1867
|
+
improvements: optimizations,
|
|
1868
|
+
performanceGain: Math.min(100, performanceGain),
|
|
1869
|
+
confidence: optimizations.length > 2 ? "high" : optimizations.length > 0 ? "medium" : "low"
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Get performance analytics across all queries
|
|
1874
|
+
*/
|
|
1875
|
+
getPerformanceAnalytics(queryHistory) {
|
|
1876
|
+
const analytics = {
|
|
1877
|
+
totalQueries: queryHistory.length,
|
|
1878
|
+
slowQueries: queryHistory.filter((q) => q.executionTimeMs > this.slowQueryThresholdMs).length,
|
|
1879
|
+
avgExecutionTime: 0,
|
|
1880
|
+
p95ExecutionTime: 0,
|
|
1881
|
+
errorRate: 0,
|
|
1882
|
+
mostFrequentTables: [],
|
|
1883
|
+
performanceTrend: "stable"
|
|
1884
|
+
};
|
|
1885
|
+
if (queryHistory.length === 0) return analytics;
|
|
1886
|
+
const executionTimes = queryHistory.map((q) => q.executionTimeMs).sort((a, b) => a - b);
|
|
1887
|
+
analytics.avgExecutionTime = executionTimes.reduce((a, b) => a + b, 0) / executionTimes.length;
|
|
1888
|
+
analytics.p95ExecutionTime = executionTimes[Math.floor(executionTimes.length * 0.95)];
|
|
1889
|
+
analytics.errorRate = queryHistory.filter((q) => q.error).length / queryHistory.length * 100;
|
|
1890
|
+
const tableUsage = /* @__PURE__ */ new Map();
|
|
1891
|
+
for (const query of queryHistory) {
|
|
1892
|
+
for (const table of query.tables) {
|
|
1893
|
+
tableUsage.set(table, (tableUsage.get(table) || 0) + 1);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
analytics.mostFrequentTables = Array.from(tableUsage.entries()).map(([table, count]) => ({ table, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1897
|
+
const midpoint = Math.floor(queryHistory.length / 2);
|
|
1898
|
+
const recentAvg = executionTimes.slice(midpoint).reduce((a, b) => a + b, 0) / (executionTimes.length - midpoint);
|
|
1899
|
+
const olderAvg = executionTimes.slice(0, midpoint).reduce((a, b) => a + b, 0) / midpoint;
|
|
1900
|
+
if (recentAvg < olderAvg * 0.8) analytics.performanceTrend = "improving";
|
|
1901
|
+
else if (recentAvg > olderAvg * 1.2) analytics.performanceTrend = "degrading";
|
|
1902
|
+
return analytics;
|
|
1903
|
+
}
|
|
1904
|
+
// Helper methods
|
|
1905
|
+
countSelectColumns(sql) {
|
|
1906
|
+
const selectMatch = sql.match(/SELECT\s+(.+?)\s+FROM/i);
|
|
1907
|
+
if (!selectMatch) return 0;
|
|
1908
|
+
const selectClause = selectMatch[1];
|
|
1909
|
+
if (selectClause.includes("*")) return 1;
|
|
1910
|
+
return (selectClause.match(/,/g) || []).length + 1;
|
|
1911
|
+
}
|
|
1912
|
+
countWhereConditions(sql) {
|
|
1913
|
+
const whereMatch = sql.match(/WHERE\s+(.+?)(?:\s+(GROUP|ORDER|LIMIT|$))/i);
|
|
1914
|
+
if (!whereMatch) return 0;
|
|
1915
|
+
const whereClause = whereMatch[1];
|
|
1916
|
+
return (whereClause.match(/\bAND\b/gi) || []).length + 1;
|
|
1917
|
+
}
|
|
1918
|
+
countJoins(sql) {
|
|
1919
|
+
return (sql.match(/\bJOIN\b/gi) || []).length;
|
|
1920
|
+
}
|
|
1921
|
+
countSubqueries(sql) {
|
|
1922
|
+
return (sql.match(/\(\s*SELECT/gi) || []).length;
|
|
1923
|
+
}
|
|
1924
|
+
extractColumnsFromCondition(condition, tables, schema) {
|
|
1925
|
+
const columns = [];
|
|
1926
|
+
const columnMatches = condition.match(/\b(\w+\.)?(\w+)\b/g) || [];
|
|
1927
|
+
for (const match of columnMatches) {
|
|
1928
|
+
if (match.includes(".")) {
|
|
1929
|
+
const [table, column] = match.split(".");
|
|
1930
|
+
if (tables.includes(table)) {
|
|
1931
|
+
columns.push({ table, column });
|
|
1932
|
+
}
|
|
1933
|
+
} else {
|
|
1934
|
+
const column = match;
|
|
1935
|
+
for (const table of tables) {
|
|
1936
|
+
const tableMeta = this.findTableInSchema(schema, table);
|
|
1937
|
+
if (tableMeta?.columns.some((col) => col.name === column)) {
|
|
1938
|
+
columns.push({ table, column });
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
return columns;
|
|
1945
|
+
}
|
|
1946
|
+
analyzeExplainPlan(plan) {
|
|
1947
|
+
const bottlenecks = [];
|
|
1948
|
+
const planStr = JSON.stringify(plan).toLowerCase();
|
|
1949
|
+
if (planStr.includes("table scan") || planStr.includes("seq scan")) {
|
|
1950
|
+
bottlenecks.push({
|
|
1951
|
+
type: "table_scan",
|
|
1952
|
+
severity: "high",
|
|
1953
|
+
description: "Full table scan detected - consider adding indexes",
|
|
1954
|
+
estimatedCost: 100
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
if (planStr.includes("sort") && !planStr.includes("index")) {
|
|
1958
|
+
bottlenecks.push({
|
|
1959
|
+
type: "sort",
|
|
1960
|
+
severity: "medium",
|
|
1961
|
+
description: "In-memory sort operation - consider indexed ORDER BY",
|
|
1962
|
+
estimatedCost: 50
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
return bottlenecks;
|
|
1966
|
+
}
|
|
1967
|
+
canRemoveDistinct(sql, _schema) {
|
|
1968
|
+
return /\bGROUP\s+BY\b/i.test(sql) || /\bPRIMARY\s+KEY\b/i.test(sql);
|
|
1969
|
+
}
|
|
1970
|
+
findTableInSchema(schema, tableName) {
|
|
1971
|
+
for (const schemaMeta of schema.schemas) {
|
|
1972
|
+
const table = schemaMeta.tables.find((t) => t.name === tableName);
|
|
1973
|
+
if (table) return table;
|
|
1974
|
+
}
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
generateQueryId(sql) {
|
|
1978
|
+
let hash = 0;
|
|
1979
|
+
for (let i = 0; i < sql.length; i++) {
|
|
1980
|
+
const char = sql.charCodeAt(i);
|
|
1981
|
+
hash = (hash << 5) - hash + char;
|
|
1982
|
+
hash = hash & hash;
|
|
1983
|
+
}
|
|
1984
|
+
return Math.abs(hash).toString(16);
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1610
1988
|
// src/query-tracker.ts
|
|
1611
1989
|
var QueryTracker = class {
|
|
1612
1990
|
history = /* @__PURE__ */ new Map();
|
|
1613
1991
|
maxHistoryPerDb = 100;
|
|
1614
|
-
|
|
1992
|
+
optimizer = new QueryOptimizer();
|
|
1993
|
+
track(dbId, sql, executionTimeMs, rowCount, error, explainPlan) {
|
|
1994
|
+
const complexity = this.optimizer.analyzeQueryComplexity(sql);
|
|
1615
1995
|
const entry = {
|
|
1616
1996
|
timestamp: /* @__PURE__ */ new Date(),
|
|
1617
1997
|
sql,
|
|
1618
1998
|
tables: extractTableNames(sql),
|
|
1619
1999
|
executionTimeMs,
|
|
1620
2000
|
rowCount,
|
|
1621
|
-
error
|
|
2001
|
+
error,
|
|
2002
|
+
explainPlan,
|
|
2003
|
+
queryComplexity: complexity,
|
|
2004
|
+
performanceScore: this.calculatePerformanceScore(executionTimeMs, complexity)
|
|
1622
2005
|
};
|
|
1623
2006
|
if (!this.history.has(dbId)) {
|
|
1624
2007
|
this.history.set(dbId, []);
|
|
@@ -1642,24 +2025,59 @@ var QueryTracker = class {
|
|
|
1642
2025
|
totalQueries: dbHistory.length,
|
|
1643
2026
|
avgExecutionTime: 0,
|
|
1644
2027
|
errorCount: 0,
|
|
1645
|
-
tableUsage: {}
|
|
2028
|
+
tableUsage: {},
|
|
2029
|
+
performanceMetrics: {
|
|
2030
|
+
avgScore: 0,
|
|
2031
|
+
slowQueryCount: 0,
|
|
2032
|
+
complexityDistribution: {}
|
|
2033
|
+
}
|
|
1646
2034
|
};
|
|
1647
2035
|
if (dbHistory.length === 0) {
|
|
1648
2036
|
return stats;
|
|
1649
2037
|
}
|
|
1650
2038
|
let totalTime = 0;
|
|
2039
|
+
let totalScore = 0;
|
|
1651
2040
|
for (const entry of dbHistory) {
|
|
1652
2041
|
totalTime += entry.executionTimeMs;
|
|
1653
2042
|
if (entry.error) {
|
|
1654
2043
|
stats.errorCount++;
|
|
1655
2044
|
}
|
|
2045
|
+
if (entry.performanceScore !== void 0) {
|
|
2046
|
+
totalScore += entry.performanceScore;
|
|
2047
|
+
}
|
|
2048
|
+
if (entry.executionTimeMs > 1e3) {
|
|
2049
|
+
stats.performanceMetrics.slowQueryCount++;
|
|
2050
|
+
}
|
|
2051
|
+
if (entry.queryComplexity) {
|
|
2052
|
+
const complexity = entry.queryComplexity.estimatedComplexity;
|
|
2053
|
+
stats.performanceMetrics.complexityDistribution[complexity] = (stats.performanceMetrics.complexityDistribution[complexity] || 0) + 1;
|
|
2054
|
+
}
|
|
1656
2055
|
for (const table of entry.tables) {
|
|
1657
2056
|
stats.tableUsage[table] = (stats.tableUsage[table] || 0) + 1;
|
|
1658
2057
|
}
|
|
1659
2058
|
}
|
|
1660
2059
|
stats.avgExecutionTime = totalTime / dbHistory.length;
|
|
2060
|
+
stats.performanceMetrics.avgScore = totalScore / dbHistory.length;
|
|
1661
2061
|
return stats;
|
|
1662
2062
|
}
|
|
2063
|
+
getPerformanceAnalytics(dbId) {
|
|
2064
|
+
const history = this.getHistory(dbId);
|
|
2065
|
+
return this.optimizer.getPerformanceAnalytics(history);
|
|
2066
|
+
}
|
|
2067
|
+
getIndexRecommendations(dbId, schema) {
|
|
2068
|
+
const history = this.getHistory(dbId);
|
|
2069
|
+
return this.optimizer.generateIndexRecommendations(history, schema);
|
|
2070
|
+
}
|
|
2071
|
+
getSlowQueryAlerts(dbId) {
|
|
2072
|
+
const history = this.getHistory(dbId);
|
|
2073
|
+
return this.optimizer.detectSlowQueries(history, dbId);
|
|
2074
|
+
}
|
|
2075
|
+
suggestQueryRewrite(sql, schema) {
|
|
2076
|
+
return this.optimizer.suggestQueryRewrites(sql, schema);
|
|
2077
|
+
}
|
|
2078
|
+
async profileQueryPerformance(dbId, sql, explainResult, executionTimeMs, rowCount) {
|
|
2079
|
+
return this.optimizer.profileQueryPerformance(dbId, sql, explainResult, executionTimeMs, rowCount);
|
|
2080
|
+
}
|
|
1663
2081
|
clear(dbId) {
|
|
1664
2082
|
if (dbId) {
|
|
1665
2083
|
this.history.delete(dbId);
|
|
@@ -1667,6 +2085,20 @@ var QueryTracker = class {
|
|
|
1667
2085
|
this.history.clear();
|
|
1668
2086
|
}
|
|
1669
2087
|
}
|
|
2088
|
+
calculatePerformanceScore(executionTimeMs, complexity) {
|
|
2089
|
+
let score = 100;
|
|
2090
|
+
if (executionTimeMs > 5e3) score -= 40;
|
|
2091
|
+
else if (executionTimeMs > 1e3) score -= 20;
|
|
2092
|
+
else if (executionTimeMs > 100) score -= 10;
|
|
2093
|
+
const complexityPenalty = {
|
|
2094
|
+
simple: 0,
|
|
2095
|
+
medium: 5,
|
|
2096
|
+
complex: 15,
|
|
2097
|
+
very_complex: 25
|
|
2098
|
+
};
|
|
2099
|
+
score -= complexityPenalty[complexity.estimatedComplexity];
|
|
2100
|
+
return Math.max(0, Math.min(100, score));
|
|
2101
|
+
}
|
|
1670
2102
|
};
|
|
1671
2103
|
|
|
1672
2104
|
// src/database-manager.ts
|
|
@@ -1794,7 +2226,15 @@ var DatabaseManager = class {
|
|
|
1794
2226
|
const adapter = this.getAdapter(dbId);
|
|
1795
2227
|
try {
|
|
1796
2228
|
const result = await adapter.query(sql, params, timeoutMs);
|
|
1797
|
-
|
|
2229
|
+
let explainPlan;
|
|
2230
|
+
if (!isWriteOperation(sql)) {
|
|
2231
|
+
try {
|
|
2232
|
+
explainPlan = await adapter.explain(sql, params);
|
|
2233
|
+
} catch (_explainError) {
|
|
2234
|
+
this.logger.debug({ dbId, sql }, "EXPLAIN failed, continuing without performance analysis");
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
this.queryTracker.track(dbId, sql, result.executionTimeMs, result.rowCount, void 0, explainPlan);
|
|
1798
2238
|
return result;
|
|
1799
2239
|
} catch (error) {
|
|
1800
2240
|
this.queryTracker.track(dbId, sql, 0, 0, error.message);
|
|
@@ -1823,6 +2263,29 @@ var DatabaseManager = class {
|
|
|
1823
2263
|
getQueryHistory(dbId, limit) {
|
|
1824
2264
|
return this.queryTracker.getHistory(dbId, limit);
|
|
1825
2265
|
}
|
|
2266
|
+
getPerformanceAnalytics(dbId) {
|
|
2267
|
+
return this.queryTracker.getPerformanceAnalytics(dbId);
|
|
2268
|
+
}
|
|
2269
|
+
async getIndexRecommendations(dbId) {
|
|
2270
|
+
const schema = await this.getSchema(dbId);
|
|
2271
|
+
return this.queryTracker.getIndexRecommendations(dbId, schema);
|
|
2272
|
+
}
|
|
2273
|
+
getSlowQueryAlerts(dbId) {
|
|
2274
|
+
return this.queryTracker.getSlowQueryAlerts(dbId);
|
|
2275
|
+
}
|
|
2276
|
+
async suggestQueryRewrite(dbId, sql) {
|
|
2277
|
+
const schema = await this.getSchema(dbId);
|
|
2278
|
+
return this.queryTracker.suggestQueryRewrite(sql, schema);
|
|
2279
|
+
}
|
|
2280
|
+
async profileQueryPerformance(dbId, sql, params = []) {
|
|
2281
|
+
await this.ensureConnected(dbId);
|
|
2282
|
+
const adapter = this.getAdapter(dbId);
|
|
2283
|
+
const startTime = Date.now();
|
|
2284
|
+
const result = await adapter.query(sql, params);
|
|
2285
|
+
const executionTimeMs = Date.now() - startTime;
|
|
2286
|
+
const explainResult = await adapter.explain(sql, params);
|
|
2287
|
+
return this.queryTracker.profileQueryPerformance(dbId, sql, explainResult, executionTimeMs, result.rowCount);
|
|
2288
|
+
}
|
|
1826
2289
|
};
|
|
1827
2290
|
|
|
1828
2291
|
// src/mcp-server.ts
|
|
@@ -1832,7 +2295,8 @@ import {
|
|
|
1832
2295
|
CallToolRequestSchema,
|
|
1833
2296
|
ListToolsRequestSchema,
|
|
1834
2297
|
ListResourcesRequestSchema,
|
|
1835
|
-
ReadResourceRequestSchema
|
|
2298
|
+
ReadResourceRequestSchema,
|
|
2299
|
+
InitializeRequestSchema
|
|
1836
2300
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1837
2301
|
var MCPServer = class {
|
|
1838
2302
|
constructor(_dbManager, _config) {
|
|
@@ -1855,6 +2319,21 @@ var MCPServer = class {
|
|
|
1855
2319
|
server;
|
|
1856
2320
|
logger = getLogger();
|
|
1857
2321
|
setupHandlers() {
|
|
2322
|
+
this.server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
2323
|
+
const { protocolVersion } = request.params;
|
|
2324
|
+
this.logger.info({ protocolVersion }, "MCP server initializing");
|
|
2325
|
+
return {
|
|
2326
|
+
protocolVersion,
|
|
2327
|
+
capabilities: {
|
|
2328
|
+
tools: {},
|
|
2329
|
+
resources: {}
|
|
2330
|
+
},
|
|
2331
|
+
serverInfo: {
|
|
2332
|
+
name: "mcp-database-server",
|
|
2333
|
+
version: "1.0.0"
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
});
|
|
1858
2337
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1859
2338
|
tools: [
|
|
1860
2339
|
{
|
|
@@ -2034,6 +2513,89 @@ var MCPServer = class {
|
|
|
2034
2513
|
}
|
|
2035
2514
|
}
|
|
2036
2515
|
}
|
|
2516
|
+
},
|
|
2517
|
+
{
|
|
2518
|
+
name: "analyze_performance",
|
|
2519
|
+
description: "Get detailed performance analytics for a database",
|
|
2520
|
+
inputSchema: {
|
|
2521
|
+
type: "object",
|
|
2522
|
+
properties: {
|
|
2523
|
+
dbId: {
|
|
2524
|
+
type: "string",
|
|
2525
|
+
description: "Database ID to analyze"
|
|
2526
|
+
}
|
|
2527
|
+
},
|
|
2528
|
+
required: ["dbId"]
|
|
2529
|
+
}
|
|
2530
|
+
},
|
|
2531
|
+
{
|
|
2532
|
+
name: "suggest_indexes",
|
|
2533
|
+
description: "Analyze query patterns and suggest optimal indexes",
|
|
2534
|
+
inputSchema: {
|
|
2535
|
+
type: "object",
|
|
2536
|
+
properties: {
|
|
2537
|
+
dbId: {
|
|
2538
|
+
type: "string",
|
|
2539
|
+
description: "Database ID to analyze"
|
|
2540
|
+
}
|
|
2541
|
+
},
|
|
2542
|
+
required: ["dbId"]
|
|
2543
|
+
}
|
|
2544
|
+
},
|
|
2545
|
+
{
|
|
2546
|
+
name: "detect_slow_queries",
|
|
2547
|
+
description: "Identify and alert on slow-running queries",
|
|
2548
|
+
inputSchema: {
|
|
2549
|
+
type: "object",
|
|
2550
|
+
properties: {
|
|
2551
|
+
dbId: {
|
|
2552
|
+
type: "string",
|
|
2553
|
+
description: "Database ID to analyze"
|
|
2554
|
+
}
|
|
2555
|
+
},
|
|
2556
|
+
required: ["dbId"]
|
|
2557
|
+
}
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
name: "rewrite_query",
|
|
2561
|
+
description: "Suggest optimized versions of SQL queries",
|
|
2562
|
+
inputSchema: {
|
|
2563
|
+
type: "object",
|
|
2564
|
+
properties: {
|
|
2565
|
+
dbId: {
|
|
2566
|
+
type: "string",
|
|
2567
|
+
description: "Database ID"
|
|
2568
|
+
},
|
|
2569
|
+
sql: {
|
|
2570
|
+
type: "string",
|
|
2571
|
+
description: "SQL query to optimize"
|
|
2572
|
+
}
|
|
2573
|
+
},
|
|
2574
|
+
required: ["dbId", "sql"]
|
|
2575
|
+
}
|
|
2576
|
+
},
|
|
2577
|
+
{
|
|
2578
|
+
name: "profile_query",
|
|
2579
|
+
description: "Profile query performance with detailed analysis",
|
|
2580
|
+
inputSchema: {
|
|
2581
|
+
type: "object",
|
|
2582
|
+
properties: {
|
|
2583
|
+
dbId: {
|
|
2584
|
+
type: "string",
|
|
2585
|
+
description: "Database ID"
|
|
2586
|
+
},
|
|
2587
|
+
sql: {
|
|
2588
|
+
type: "string",
|
|
2589
|
+
description: "SQL query to profile"
|
|
2590
|
+
},
|
|
2591
|
+
params: {
|
|
2592
|
+
type: "array",
|
|
2593
|
+
description: "Query parameters",
|
|
2594
|
+
items: {}
|
|
2595
|
+
}
|
|
2596
|
+
},
|
|
2597
|
+
required: ["dbId", "sql"]
|
|
2598
|
+
}
|
|
2037
2599
|
}
|
|
2038
2600
|
]
|
|
2039
2601
|
}));
|
|
@@ -2059,6 +2621,16 @@ var MCPServer = class {
|
|
|
2059
2621
|
return await this.handleCacheStatus(args);
|
|
2060
2622
|
case "health_check":
|
|
2061
2623
|
return await this.handleHealthCheck(args);
|
|
2624
|
+
case "analyze_performance":
|
|
2625
|
+
return await this.handleAnalyzePerformance(args);
|
|
2626
|
+
case "suggest_indexes":
|
|
2627
|
+
return await this.handleSuggestIndexes(args);
|
|
2628
|
+
case "detect_slow_queries":
|
|
2629
|
+
return await this.handleDetectSlowQueries(args);
|
|
2630
|
+
case "rewrite_query":
|
|
2631
|
+
return await this.handleRewriteQuery(args);
|
|
2632
|
+
case "profile_query":
|
|
2633
|
+
return await this.handleProfileQuery(args);
|
|
2062
2634
|
default:
|
|
2063
2635
|
throw new Error(`Unknown tool: ${name}`);
|
|
2064
2636
|
}
|
|
@@ -2294,9 +2866,67 @@ var MCPServer = class {
|
|
|
2294
2866
|
]
|
|
2295
2867
|
};
|
|
2296
2868
|
}
|
|
2869
|
+
async handleAnalyzePerformance(args) {
|
|
2870
|
+
const analytics = this._dbManager.getPerformanceAnalytics(args.dbId);
|
|
2871
|
+
return {
|
|
2872
|
+
content: [
|
|
2873
|
+
{
|
|
2874
|
+
type: "text",
|
|
2875
|
+
text: JSON.stringify(analytics, null, 2)
|
|
2876
|
+
}
|
|
2877
|
+
]
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
async handleSuggestIndexes(args) {
|
|
2881
|
+
const recommendations = await this._dbManager.getIndexRecommendations(args.dbId);
|
|
2882
|
+
return {
|
|
2883
|
+
content: [
|
|
2884
|
+
{
|
|
2885
|
+
type: "text",
|
|
2886
|
+
text: JSON.stringify(recommendations, null, 2)
|
|
2887
|
+
}
|
|
2888
|
+
]
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
async handleDetectSlowQueries(args) {
|
|
2892
|
+
const alerts = this._dbManager.getSlowQueryAlerts(args.dbId);
|
|
2893
|
+
return {
|
|
2894
|
+
content: [
|
|
2895
|
+
{
|
|
2896
|
+
type: "text",
|
|
2897
|
+
text: JSON.stringify(alerts, null, 2)
|
|
2898
|
+
}
|
|
2899
|
+
]
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
async handleRewriteQuery(args) {
|
|
2903
|
+
const suggestion = await this._dbManager.suggestQueryRewrite(args.dbId, args.sql);
|
|
2904
|
+
return {
|
|
2905
|
+
content: [
|
|
2906
|
+
{
|
|
2907
|
+
type: "text",
|
|
2908
|
+
text: JSON.stringify(suggestion, null, 2)
|
|
2909
|
+
}
|
|
2910
|
+
]
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
async handleProfileQuery(args) {
|
|
2914
|
+
const profile = await this._dbManager.profileQueryPerformance(args.dbId, args.sql, args.params);
|
|
2915
|
+
return {
|
|
2916
|
+
content: [
|
|
2917
|
+
{
|
|
2918
|
+
type: "text",
|
|
2919
|
+
text: JSON.stringify(profile, null, 2)
|
|
2920
|
+
}
|
|
2921
|
+
]
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2297
2924
|
async start() {
|
|
2925
|
+
console.error("Starting MCP server...");
|
|
2298
2926
|
const transport = new StdioServerTransport();
|
|
2927
|
+
console.error("Created transport, connecting...");
|
|
2299
2928
|
await this.server.connect(transport);
|
|
2929
|
+
console.error("MCP server connected and started");
|
|
2300
2930
|
this.logger.info("MCP server started");
|
|
2301
2931
|
}
|
|
2302
2932
|
};
|
|
@@ -2315,8 +2945,7 @@ async function main() {
|
|
|
2315
2945
|
options: {
|
|
2316
2946
|
config: {
|
|
2317
2947
|
type: "string",
|
|
2318
|
-
short: "c"
|
|
2319
|
-
default: "./.mcp-database-server.config"
|
|
2948
|
+
short: "c"
|
|
2320
2949
|
},
|
|
2321
2950
|
help: {
|
|
2322
2951
|
type: "boolean",
|
|
@@ -2340,7 +2969,7 @@ Usage:
|
|
|
2340
2969
|
mcp-database-server [options]
|
|
2341
2970
|
|
|
2342
2971
|
Options:
|
|
2343
|
-
-c, --config <path> Path to configuration file (
|
|
2972
|
+
-c, --config <path> Path to configuration file (if not specified, searches for .mcp-database-server.config from project root upwards)
|
|
2344
2973
|
-h, --help Show this help message
|
|
2345
2974
|
-v, --version Show version number
|
|
2346
2975
|
|
|
@@ -2358,13 +2987,19 @@ Examples:
|
|
|
2358
2987
|
`);
|
|
2359
2988
|
process.exit(0);
|
|
2360
2989
|
}
|
|
2361
|
-
let configPath
|
|
2362
|
-
if (
|
|
2990
|
+
let configPath;
|
|
2991
|
+
if (values.config) {
|
|
2992
|
+
configPath = values.config;
|
|
2993
|
+
if (!existsSync2(configPath)) {
|
|
2994
|
+
console.error(`Error: Specified config file ${configPath} not found`);
|
|
2995
|
+
process.exit(1);
|
|
2996
|
+
}
|
|
2997
|
+
} else {
|
|
2363
2998
|
const foundPath = findConfigFile(".mcp-database-server.config");
|
|
2364
2999
|
if (foundPath) {
|
|
2365
3000
|
configPath = foundPath;
|
|
2366
3001
|
} else {
|
|
2367
|
-
console.error("Error:
|
|
3002
|
+
console.error("Error: No config file specified, and .mcp-database-server.config not found");
|
|
2368
3003
|
console.error("Searched in current directory and all parent directories");
|
|
2369
3004
|
console.error("\nTo create a config file:");
|
|
2370
3005
|
console.error(" cp mcp-database-server.config.example .mcp-database-server.config");
|