@ecubelabs/mysql-mcp 0.0.0-stage → 0.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/LICENSE.md +21 -0
- package/README.md +848 -2
- package/dist/index.js +684 -0
- package/dist/src/catalog/collect.js +220 -0
- package/dist/src/catalog/docs.js +328 -0
- package/dist/src/catalog/index.js +685 -0
- package/dist/src/catalog/render.js +177 -0
- package/dist/src/catalog/store.js +617 -0
- package/dist/src/catalog/types.js +59 -0
- package/dist/src/catalog/usage.js +164 -0
- package/dist/src/config/index.js +244 -0
- package/dist/src/db/diagnose.js +189 -0
- package/dist/src/db/index.js +461 -0
- package/dist/src/db/permissions.js +26 -0
- package/dist/src/db/utils.js +264 -0
- package/dist/src/ssh/config.js +137 -0
- package/dist/src/ssh/tunnel.js +334 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils/index.js +11 -0
- package/package.json +50 -3
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const UNLINKED_SAMPLE_LIMIT = 30;
|
|
2
|
+
const HOT_TABLE_LIMIT = 10;
|
|
3
|
+
function sampled(values) {
|
|
4
|
+
const sample = values.slice(0, UNLINKED_SAMPLE_LIMIT);
|
|
5
|
+
return {
|
|
6
|
+
total: values.length,
|
|
7
|
+
sample,
|
|
8
|
+
...(values.length > sample.length ? { truncated: true } : {}),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function usedAt(table) {
|
|
12
|
+
const parsed = Date.parse(table.usage.lastUsedAt ?? '');
|
|
13
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
14
|
+
}
|
|
15
|
+
function rankTables(tables) {
|
|
16
|
+
return tables.sort((a, b) => {
|
|
17
|
+
const used = b.entry.usage.successCount - a.entry.usage.successCount;
|
|
18
|
+
if (used) {
|
|
19
|
+
return used;
|
|
20
|
+
}
|
|
21
|
+
const recent = usedAt(b.entry) - usedAt(a.entry);
|
|
22
|
+
if (recent) {
|
|
23
|
+
return recent;
|
|
24
|
+
}
|
|
25
|
+
const rows = (b.entry.rowsEstimate ?? 0) - (a.entry.rowsEstimate ?? 0);
|
|
26
|
+
if (rows) {
|
|
27
|
+
return rows;
|
|
28
|
+
}
|
|
29
|
+
return `${a.schema}.${a.table}`.localeCompare(`${b.schema}.${b.table}`);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function allTables(catalog) {
|
|
33
|
+
return Object.entries(catalog.schemas).flatMap(([schema, entry]) => Object.entries(entry.tables).map(([table, tableEntry]) => ({
|
|
34
|
+
app: entry.app,
|
|
35
|
+
schema,
|
|
36
|
+
table,
|
|
37
|
+
entry: tableEntry,
|
|
38
|
+
})));
|
|
39
|
+
}
|
|
40
|
+
export const HOT_TABLES_HEADING = '\n\n카탈로그가 많이 읽은 테이블(읽은 순): ';
|
|
41
|
+
export function renderToolDescriptionSuffix(catalog, budget) {
|
|
42
|
+
const staticGuidance = '\n\n테이블의 도메인 규칙·상태 코드 문서가 있을 수 있다. ' +
|
|
43
|
+
'SQL을 쓰기 전에 mysql_catalog describe로 확인하라.';
|
|
44
|
+
if (staticGuidance.length > budget) {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
const hot = rankTables(allTables(catalog).filter(({ entry }) => entry.usage.successCount > 0))
|
|
48
|
+
.slice(0, HOT_TABLE_LIMIT)
|
|
49
|
+
.map(({ schema, table }) => `${schema}.${table}`);
|
|
50
|
+
if (hot.length === 0) {
|
|
51
|
+
return staticGuidance;
|
|
52
|
+
}
|
|
53
|
+
let listed = '';
|
|
54
|
+
for (const name of hot) {
|
|
55
|
+
const next = listed ? `${listed}, ${name}` : name;
|
|
56
|
+
if ((HOT_TABLES_HEADING + next + staticGuidance).length > budget) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
listed = next;
|
|
60
|
+
}
|
|
61
|
+
if (!listed) {
|
|
62
|
+
return staticGuidance;
|
|
63
|
+
}
|
|
64
|
+
return HOT_TABLES_HEADING + listed + staticGuidance;
|
|
65
|
+
}
|
|
66
|
+
export function renderMap(catalog, warning = null) {
|
|
67
|
+
const schemas = Object.entries(catalog.schemas).map(([name, schema]) => ({
|
|
68
|
+
app: schema.app,
|
|
69
|
+
schema: name,
|
|
70
|
+
description: schema.description ?? '',
|
|
71
|
+
scannedAt: schema.scannedAt,
|
|
72
|
+
tableCount: Object.keys(schema.tables).length,
|
|
73
|
+
tables: rankTables(Object.entries(schema.tables).map(([table, entry]) => ({
|
|
74
|
+
app: schema.app,
|
|
75
|
+
schema: name,
|
|
76
|
+
table,
|
|
77
|
+
entry,
|
|
78
|
+
})))
|
|
79
|
+
.slice(0, 10)
|
|
80
|
+
.map(({ table, entry }) => ({
|
|
81
|
+
table,
|
|
82
|
+
rowsEstimate: entry.rowsEstimate,
|
|
83
|
+
usageCount: entry.usage.count,
|
|
84
|
+
successCount: entry.usage.successCount,
|
|
85
|
+
lastUsedAt: entry.usage.lastUsedAt,
|
|
86
|
+
})),
|
|
87
|
+
}));
|
|
88
|
+
const unlinkedTables = catalog.docs.repo
|
|
89
|
+
? Object.entries(catalog.schemas).flatMap(([schemaName, schema]) => Object.entries(schema.tables)
|
|
90
|
+
.filter(([, table]) => !Object.prototype.hasOwnProperty.call(table.curated, 'doc'))
|
|
91
|
+
.map(([tableName]) => `${schemaName}.${tableName}`))
|
|
92
|
+
: [];
|
|
93
|
+
return JSON.stringify({
|
|
94
|
+
profile: catalog.profile,
|
|
95
|
+
schemas,
|
|
96
|
+
unlinkedTables: sampled(unlinkedTables),
|
|
97
|
+
unlinkedDocuments: sampled(catalog.docs.unlinked),
|
|
98
|
+
...(warning ? { warning } : {}),
|
|
99
|
+
}, null, 2);
|
|
100
|
+
}
|
|
101
|
+
function matchScore(value, query, base) {
|
|
102
|
+
const candidate = value.toLowerCase();
|
|
103
|
+
if (candidate === query) {
|
|
104
|
+
return base + 30;
|
|
105
|
+
}
|
|
106
|
+
if (candidate.startsWith(query)) {
|
|
107
|
+
return base + 20;
|
|
108
|
+
}
|
|
109
|
+
if (candidate.includes(query)) {
|
|
110
|
+
return base + 10;
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
export function searchCatalog(catalog, query, limit) {
|
|
115
|
+
const q = query.trim().toLowerCase();
|
|
116
|
+
if (!q) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const [schemaName, schema] of Object.entries(catalog.schemas)) {
|
|
121
|
+
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
122
|
+
const qualified = `${schemaName}.${tableName}`;
|
|
123
|
+
const matchedColumns = table.columns
|
|
124
|
+
.filter((column) => matchScore(column.name, q, 40) > 0)
|
|
125
|
+
.map((column) => column.name);
|
|
126
|
+
let score = Math.max(matchScore(qualified, q, 80), matchScore(tableName, q, 70), matchScore(table.comment, q, 20), ...table.curated.aliases.map((alias) => matchScore(alias, q, 60)), ...table.curated.notes.map((note) => matchScore(note, q, 10)), ...matchedColumns.map((column) => matchScore(column, q, 40)));
|
|
127
|
+
if (score === 0) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
score += Math.min(9, Math.floor(Math.log2(table.usage.successCount + 1)));
|
|
131
|
+
results.push({
|
|
132
|
+
table: qualified,
|
|
133
|
+
score,
|
|
134
|
+
comment: table.comment,
|
|
135
|
+
matchedColumns,
|
|
136
|
+
aliases: table.curated.aliases,
|
|
137
|
+
notes: table.curated.notes,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return results.sort((a, b) => b.score - a.score || a.table.localeCompare(b.table)).slice(0, limit);
|
|
142
|
+
}
|
|
143
|
+
export function renderDescribe(qualifiedName, table, documents, observedJoins) {
|
|
144
|
+
let docs;
|
|
145
|
+
const hasDocumentDecision = Object.prototype.hasOwnProperty.call(table.curated, 'doc');
|
|
146
|
+
const documentLink = table.curated.doc;
|
|
147
|
+
if (!documents.configured) {
|
|
148
|
+
docs = '문서 축 비활성 (MYSQL_DOCS_REPO 미설정)';
|
|
149
|
+
}
|
|
150
|
+
else if (!documents.available) {
|
|
151
|
+
docs = '문서 축 비활성';
|
|
152
|
+
}
|
|
153
|
+
else if (!hasDocumentDecision) {
|
|
154
|
+
docs =
|
|
155
|
+
'연결 안 됨\n' +
|
|
156
|
+
`후보를 보려면 mysql_catalog {action:"docs_list", schema:"${documents.schema}"}\n` +
|
|
157
|
+
'status 같은 코드 컬럼의 의미는 도메인 문서에만 있습니다.';
|
|
158
|
+
}
|
|
159
|
+
else if (documentLink == null) {
|
|
160
|
+
docs = '문서 없음 확인됨';
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
docs = {
|
|
164
|
+
path: documentLink.path,
|
|
165
|
+
ref: documents.ref,
|
|
166
|
+
command: documents.command,
|
|
167
|
+
guidance: '상태 코드·도메인 규칙은 이 문서를 먼저 읽어라.',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return JSON.stringify({
|
|
171
|
+
table: qualifiedName,
|
|
172
|
+
...table,
|
|
173
|
+
...(observedJoins.length > 0 ? { observedJoins } : {}),
|
|
174
|
+
docs,
|
|
175
|
+
...(documents.warning ? { warning: documents.warning } : {}),
|
|
176
|
+
}, null, 2);
|
|
177
|
+
}
|