mcp-migration-advisor 0.2.0

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.
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Liquibase YAML changelog parser.
3
+ *
4
+ * Parses Liquibase YAML changelogs and extracts DDL operations
5
+ * compatible with the same DDLStatement interface used by Flyway and XML.
6
+ */
7
+ /**
8
+ * Parse a Liquibase YAML changelog and extract DDL statements.
9
+ *
10
+ * Supports: createTable, dropTable, addColumn, dropColumn, modifyDataType,
11
+ * addNotNullConstraint, createIndex, dropIndex, addForeignKeyConstraint,
12
+ * dropForeignKeyConstraint, renameTable, renameColumn, sql (raw).
13
+ */
14
+ export function parseLiquibaseYaml(yaml) {
15
+ const changeSets = extractChangeSets(yaml);
16
+ const allStatements = [];
17
+ for (const cs of changeSets) {
18
+ allStatements.push(...cs.statements);
19
+ }
20
+ return {
21
+ version: changeSets.length > 0 ? changeSets[0].id : null,
22
+ description: `Liquibase changelog (${changeSets.length} changeSets)`,
23
+ filename: "changelog.yaml",
24
+ isRepeatable: false,
25
+ statements: allStatements,
26
+ };
27
+ }
28
+ function extractChangeSets(yaml) {
29
+ const results = [];
30
+ // Split into lines for indentation-based parsing
31
+ const lines = yaml.split("\n");
32
+ // Find changeSet blocks by scanning for "- changeSet:" pattern
33
+ let i = 0;
34
+ while (i < lines.length) {
35
+ const line = lines[i];
36
+ const csMatch = line.match(/^(\s*)-\s*changeSet\s*:/);
37
+ if (!csMatch) {
38
+ i++;
39
+ continue;
40
+ }
41
+ const baseIndent = csMatch[1].length;
42
+ // Collect all lines belonging to this changeSet
43
+ const csLines = [];
44
+ i++;
45
+ while (i < lines.length) {
46
+ const nextLine = lines[i];
47
+ // Empty lines are part of the block
48
+ if (nextLine.trim() === "") {
49
+ csLines.push(nextLine);
50
+ i++;
51
+ continue;
52
+ }
53
+ // Check if we've exited the changeSet block
54
+ const nextIndent = nextLine.search(/\S/);
55
+ if (nextIndent <= baseIndent && nextLine.trim().startsWith("-"))
56
+ break;
57
+ if (nextIndent <= baseIndent && !nextLine.trim().startsWith("-"))
58
+ break;
59
+ csLines.push(nextLine);
60
+ i++;
61
+ }
62
+ const csBody = csLines.join("\n");
63
+ const id = extractValue(csBody, "id") || "unknown";
64
+ const author = extractValue(csBody, "author") || "unknown";
65
+ const statements = parseChanges(csBody);
66
+ results.push({ id, author, statements });
67
+ }
68
+ return results;
69
+ }
70
+ function extractValue(text, key) {
71
+ // Match key: value or key: "value" (with optional quotes)
72
+ const re = new RegExp(`\\b${key}\\s*:\\s*["']?([^"'\\n]+?)["']?\\s*$`, "m");
73
+ const m = text.match(re);
74
+ return m ? m[1].trim() : null;
75
+ }
76
+ function parseChanges(csBody) {
77
+ const statements = [];
78
+ // Find the "changes:" block
79
+ const changesIdx = csBody.indexOf("changes:");
80
+ if (changesIdx === -1)
81
+ return statements;
82
+ const changesBody = csBody.substring(changesIdx);
83
+ // Parse each change type
84
+ parseCreateTable(changesBody, statements);
85
+ parseDropTable(changesBody, statements);
86
+ parseAddColumn(changesBody, statements);
87
+ parseDropColumn(changesBody, statements);
88
+ parseModifyDataType(changesBody, statements);
89
+ parseAddNotNullConstraint(changesBody, statements);
90
+ parseCreateIndex(changesBody, statements);
91
+ parseDropIndex(changesBody, statements);
92
+ parseAddForeignKeyConstraint(changesBody, statements);
93
+ parseDropForeignKeyConstraint(changesBody, statements);
94
+ parseRenameTable(changesBody, statements);
95
+ parseRenameColumn(changesBody, statements);
96
+ parseRawSql(changesBody, statements);
97
+ return statements;
98
+ }
99
+ function extractChangeBlock(body, changeName) {
100
+ const blocks = [];
101
+ const re = new RegExp(`-\\s*${changeName}\\s*:`, "g");
102
+ let m;
103
+ while ((m = re.exec(body)) !== null) {
104
+ const startIdx = m.index + m[0].length;
105
+ const blockIndent = body.substring(0, m.index).split("\n").pop().search(/\S/);
106
+ const remaining = body.substring(startIdx);
107
+ const lines = remaining.split("\n");
108
+ const blockLines = [];
109
+ for (let i = 0; i < lines.length; i++) {
110
+ const line = lines[i];
111
+ if (line.trim() === "") {
112
+ blockLines.push(line);
113
+ continue;
114
+ }
115
+ const indent = line.search(/\S/);
116
+ // If we hit something at same or lower indent that starts a new change, stop
117
+ if (i > 0 && indent <= blockIndent + 2 && line.trim().startsWith("-"))
118
+ break;
119
+ if (i > 0 && indent <= blockIndent)
120
+ break;
121
+ blockLines.push(line);
122
+ }
123
+ blocks.push(blockLines.join("\n"));
124
+ }
125
+ return blocks;
126
+ }
127
+ function parseCreateTable(body, statements) {
128
+ for (const block of extractChangeBlock(body, "createTable")) {
129
+ const tableName = extractValue(block, "tableName") || "unknown";
130
+ const columns = extractColumnNames(block);
131
+ const details = {};
132
+ if (columns.length > 0)
133
+ details.columns = columns.join(", ");
134
+ statements.push({
135
+ type: "CREATE_TABLE",
136
+ raw: `createTable: ${tableName}`,
137
+ tableName,
138
+ columnName: null,
139
+ details,
140
+ });
141
+ }
142
+ }
143
+ function parseDropTable(body, statements) {
144
+ for (const block of extractChangeBlock(body, "dropTable")) {
145
+ const tableName = extractValue(block, "tableName") || "unknown";
146
+ const details = {};
147
+ const cascade = extractValue(block, "cascadeConstraints");
148
+ if (cascade === "true")
149
+ details.cascade = "true";
150
+ statements.push({
151
+ type: "DROP_TABLE",
152
+ raw: `dropTable: ${tableName}`,
153
+ tableName,
154
+ columnName: null,
155
+ details,
156
+ });
157
+ }
158
+ }
159
+ function parseAddColumn(body, statements) {
160
+ for (const block of extractChangeBlock(body, "addColumn")) {
161
+ const tableName = extractValue(block, "tableName") || "unknown";
162
+ // Find column entries
163
+ const colNames = extractColumnEntries(block);
164
+ for (const col of colNames) {
165
+ const details = {};
166
+ if (col.type)
167
+ details.type = col.type;
168
+ if (col.nullable === "false")
169
+ details.notNull = "true";
170
+ if (col.defaultValue)
171
+ details.hasDefault = "true";
172
+ statements.push({
173
+ type: "ADD_COLUMN",
174
+ raw: `addColumn: ${tableName}.${col.name}`,
175
+ tableName,
176
+ columnName: col.name,
177
+ details,
178
+ });
179
+ }
180
+ }
181
+ }
182
+ function parseDropColumn(body, statements) {
183
+ for (const block of extractChangeBlock(body, "dropColumn")) {
184
+ const tableName = extractValue(block, "tableName") || "unknown";
185
+ const colName = extractValue(block, "columnName") || "unknown";
186
+ statements.push({
187
+ type: "DROP_COLUMN",
188
+ raw: `dropColumn: ${tableName}.${colName}`,
189
+ tableName,
190
+ columnName: colName,
191
+ details: {},
192
+ });
193
+ }
194
+ }
195
+ function parseModifyDataType(body, statements) {
196
+ for (const block of extractChangeBlock(body, "modifyDataType")) {
197
+ const tableName = extractValue(block, "tableName") || "unknown";
198
+ const colName = extractValue(block, "columnName") || "unknown";
199
+ const newType = extractValue(block, "newDataType");
200
+ const details = { typeChange: "true" };
201
+ if (newType)
202
+ details.newType = newType;
203
+ statements.push({
204
+ type: "MODIFY_COLUMN",
205
+ raw: `modifyDataType: ${tableName}.${colName}`,
206
+ tableName,
207
+ columnName: colName,
208
+ details,
209
+ });
210
+ }
211
+ }
212
+ function parseAddNotNullConstraint(body, statements) {
213
+ for (const block of extractChangeBlock(body, "addNotNullConstraint")) {
214
+ const tableName = extractValue(block, "tableName") || "unknown";
215
+ const colName = extractValue(block, "columnName") || "unknown";
216
+ const details = { setNotNull: "true" };
217
+ const defaultVal = extractValue(block, "defaultNullValue");
218
+ if (defaultVal)
219
+ details.hasDefault = "true";
220
+ statements.push({
221
+ type: "MODIFY_COLUMN",
222
+ raw: `addNotNullConstraint: ${tableName}.${colName}`,
223
+ tableName,
224
+ columnName: colName,
225
+ details,
226
+ });
227
+ }
228
+ }
229
+ function parseCreateIndex(body, statements) {
230
+ for (const block of extractChangeBlock(body, "createIndex")) {
231
+ const tableName = extractValue(block, "tableName") || "unknown";
232
+ const details = {};
233
+ if (extractValue(block, "unique") === "true")
234
+ details.unique = "true";
235
+ statements.push({
236
+ type: "CREATE_INDEX",
237
+ raw: `createIndex: ${tableName}`,
238
+ tableName,
239
+ columnName: null,
240
+ details,
241
+ });
242
+ }
243
+ }
244
+ function parseDropIndex(body, statements) {
245
+ for (const block of extractChangeBlock(body, "dropIndex")) {
246
+ const tableName = extractValue(block, "tableName") || null;
247
+ statements.push({
248
+ type: "DROP_INDEX",
249
+ raw: `dropIndex`,
250
+ tableName,
251
+ columnName: null,
252
+ details: {},
253
+ });
254
+ }
255
+ }
256
+ function parseAddForeignKeyConstraint(body, statements) {
257
+ for (const block of extractChangeBlock(body, "addForeignKeyConstraint")) {
258
+ const tableName = extractValue(block, "baseTableName") || "unknown";
259
+ const constraintName = extractValue(block, "constraintName") || "unknown";
260
+ statements.push({
261
+ type: "ADD_CONSTRAINT",
262
+ raw: `addForeignKeyConstraint: ${constraintName}`,
263
+ tableName,
264
+ columnName: null,
265
+ details: { constraintName, constraintType: "FOREIGN_KEY" },
266
+ });
267
+ }
268
+ }
269
+ function parseDropForeignKeyConstraint(body, statements) {
270
+ for (const block of extractChangeBlock(body, "dropForeignKeyConstraint")) {
271
+ const tableName = extractValue(block, "baseTableName") || "unknown";
272
+ const constraintName = extractValue(block, "constraintName") || "unknown";
273
+ statements.push({
274
+ type: "DROP_CONSTRAINT",
275
+ raw: `dropForeignKeyConstraint: ${constraintName}`,
276
+ tableName,
277
+ columnName: null,
278
+ details: { constraintName },
279
+ });
280
+ }
281
+ }
282
+ function parseRenameTable(body, statements) {
283
+ for (const block of extractChangeBlock(body, "renameTable")) {
284
+ const oldName = extractValue(block, "oldTableName");
285
+ const newName = extractValue(block, "newTableName");
286
+ statements.push({
287
+ type: "RENAME",
288
+ raw: `renameTable: ${oldName} -> ${newName}`,
289
+ tableName: oldName || "unknown",
290
+ columnName: null,
291
+ details: { newName: newName || "unknown" },
292
+ });
293
+ }
294
+ }
295
+ function parseRenameColumn(body, statements) {
296
+ for (const block of extractChangeBlock(body, "renameColumn")) {
297
+ const tableName = extractValue(block, "tableName") || "unknown";
298
+ const oldName = extractValue(block, "oldColumnName");
299
+ const newName = extractValue(block, "newColumnName");
300
+ statements.push({
301
+ type: "RENAME",
302
+ raw: `renameColumn: ${tableName}.${oldName} -> ${newName}`,
303
+ tableName,
304
+ columnName: oldName || null,
305
+ details: { newName: newName || "unknown" },
306
+ });
307
+ }
308
+ }
309
+ function parseRawSql(body, statements) {
310
+ for (const block of extractChangeBlock(body, "sql")) {
311
+ // The block content after "- sql:" is the raw SQL
312
+ const sql = block.trim();
313
+ if (sql) {
314
+ statements.push({
315
+ type: "OTHER",
316
+ raw: sql,
317
+ tableName: null,
318
+ columnName: null,
319
+ details: { source: "inline-sql" },
320
+ });
321
+ }
322
+ }
323
+ }
324
+ function extractColumnEntries(block) {
325
+ const columns = [];
326
+ // Find "- column:" blocks
327
+ const colRe = /-\s*column\s*:/g;
328
+ let m;
329
+ const lines = block.split("\n");
330
+ let lineIdx = 0;
331
+ while (lineIdx < lines.length) {
332
+ const line = lines[lineIdx];
333
+ if (!line.match(/-\s*column\s*:/)) {
334
+ lineIdx++;
335
+ continue;
336
+ }
337
+ // Collect column block
338
+ const colIndent = line.search(/\S/);
339
+ const colLines = [];
340
+ lineIdx++;
341
+ while (lineIdx < lines.length) {
342
+ const next = lines[lineIdx];
343
+ if (next.trim() === "") {
344
+ colLines.push(next);
345
+ lineIdx++;
346
+ continue;
347
+ }
348
+ const nextIndent = next.search(/\S/);
349
+ if (nextIndent <= colIndent)
350
+ break;
351
+ colLines.push(next);
352
+ lineIdx++;
353
+ }
354
+ const colBody = colLines.join("\n");
355
+ const name = extractValue(colBody, "name");
356
+ if (name) {
357
+ const entry = { name };
358
+ const type = extractValue(colBody, "type");
359
+ if (type)
360
+ entry.type = type;
361
+ // Check constraints block for nullable
362
+ if (colBody.includes("nullable:")) {
363
+ const nullable = extractValue(colBody, "nullable");
364
+ if (nullable)
365
+ entry.nullable = nullable;
366
+ }
367
+ // Check for default values
368
+ const defaultValue = extractValue(colBody, "defaultValue") ||
369
+ extractValue(colBody, "defaultValueNumeric") ||
370
+ extractValue(colBody, "defaultValueBoolean");
371
+ if (defaultValue)
372
+ entry.defaultValue = defaultValue;
373
+ columns.push(entry);
374
+ }
375
+ }
376
+ return columns;
377
+ }
378
+ function extractColumnNames(block) {
379
+ return extractColumnEntries(block).map((c) => c.name);
380
+ }
381
+ //# sourceMappingURL=liquibase-yaml.js.map
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "mcp-migration-advisor",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for database migration risk analysis — Flyway and Liquibase XML/YAML/SQL support with lock detection and conflict analysis",
5
+ "main": "build/index.js",
6
+ "bin": {
7
+ "mcp-migration-advisor": "build/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc && chmod 755 build/index.js",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "prepublishOnly": "npm run build && npm test"
15
+ },
16
+ "files": [
17
+ "build/**/*.js",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "database",
25
+ "migration",
26
+ "flyway",
27
+ "liquibase",
28
+ "schema",
29
+ "risk-analysis",
30
+ "postgresql",
31
+ "mysql",
32
+ "ai",
33
+ "claude"
34
+ ],
35
+ "author": "Dmytro Lisnichenko",
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/Dmitriusan/mcp-migration-advisor"
43
+ },
44
+ "homepage": "https://github.com/Dmitriusan/mcp-migration-advisor#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/Dmitriusan/mcp-migration-advisor/issues"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.27.1",
50
+ "zod": "^3.24.2"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.0.0",
54
+ "typescript": "^5.8.2",
55
+ "vitest": "^4.0.18"
56
+ }
57
+ }