mcp-migration-advisor 0.2.9 → 0.2.10

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.
Files changed (2) hide show
  1. package/build/index.js +214 -156
  2. package/package.json +1 -1
package/build/index.js CHANGED
@@ -33,7 +33,7 @@ function formatParserWarnings(migration) {
33
33
  }
34
34
  // Handle --help
35
35
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
36
- console.log(`mcp-migration-advisor v0.2.9 — MCP server for database migration risk analysis
36
+ console.log(`mcp-migration-advisor v0.2.10 — MCP server for database migration risk analysis
37
37
 
38
38
  Usage:
39
39
  mcp-migration-advisor [options]
@@ -52,177 +52,216 @@ Tools provided:
52
52
  }
53
53
  const server = new McpServer({
54
54
  name: "mcp-migration-advisor",
55
- version: "0.2.9",
55
+ version: "0.2.10",
56
56
  });
57
57
  // Tool 1: analyze_migration
58
58
  server.tool("analyze_migration", "Analyze a SQL migration file for lock risks, data loss potential, and unsafe patterns. Supports Flyway (V__*.sql) and plain SQL.", {
59
59
  filename: z.string().describe("Migration filename (e.g., V2__add_user_email.sql)"),
60
60
  sql: z.string().describe("The SQL content of the migration file"),
61
61
  }, async ({ filename, sql }) => {
62
- const migration = parseMigration(filename, sql);
63
- const lockRisks = analyzeLockRisks(migration);
64
- const dataLossIssues = analyzeDataLoss(migration);
65
- const lockScore = calculateRiskScore(lockRisks);
66
- const dataLossScore = Math.min(100, dataLossIssues.filter(i => i.risk === "CERTAIN").length * 25 +
67
- dataLossIssues.filter(i => i.risk === "LIKELY").length * 15 +
68
- dataLossIssues.filter(i => i.risk === "POSSIBLE").length * 5);
69
- const riskScore = Math.min(100, lockScore + dataLossScore);
70
- let output = `## Migration Analysis: ${filename}\n\n`;
71
- // Migration info
72
- if (migration.version) {
73
- output += `**Version**: ${migration.version}\n`;
74
- }
75
- output += `**Description**: ${migration.description}\n`;
76
- output += `**Statements**: ${migration.statements.length}\n`;
77
- output += `**Risk Score**: ${riskScore}/100${riskScore >= 60 ? " ⚠️ HIGH RISK" : riskScore >= 30 ? " ⚡ MODERATE RISK" : " ✅ LOW RISK"}\n\n`;
78
- // Statement summary
79
- output += "### Operations\n\n";
80
- const typeCounts = {};
81
- for (const stmt of migration.statements) {
82
- typeCounts[stmt.type] = (typeCounts[stmt.type] || 0) + 1;
83
- }
84
- output += "| Operation | Count |\n|-----------|-------|\n";
85
- for (const [type, count] of Object.entries(typeCounts)) {
86
- output += `| ${type} | ${count} |\n`;
87
- }
88
- output += "\n";
89
- // Lock risks
90
- if (lockRisks.length > 0) {
91
- output += "### Lock Risks\n\n";
92
- for (const risk of lockRisks) {
93
- output += `**${risk.severity}**: ${risk.risk}\n`;
94
- output += `> \`${risk.statement}\`\n`;
95
- output += `> **Recommendation**: ${risk.recommendation}\n\n`;
62
+ try {
63
+ const migration = parseMigration(filename, sql);
64
+ const lockRisks = analyzeLockRisks(migration);
65
+ const dataLossIssues = analyzeDataLoss(migration);
66
+ const lockScore = calculateRiskScore(lockRisks);
67
+ const dataLossScore = Math.min(100, dataLossIssues.filter(i => i.risk === "CERTAIN").length * 25 +
68
+ dataLossIssues.filter(i => i.risk === "LIKELY").length * 15 +
69
+ dataLossIssues.filter(i => i.risk === "POSSIBLE").length * 5);
70
+ const riskScore = Math.min(100, lockScore + dataLossScore);
71
+ let output = `## Migration Analysis: ${filename}\n\n`;
72
+ // Migration info
73
+ if (migration.version) {
74
+ output += `**Version**: ${migration.version}\n`;
96
75
  }
97
- }
98
- else {
99
- output += "### Lock Risks\n\nNo lock risks detected.\n\n";
100
- }
101
- // Data loss issues
102
- if (dataLossIssues.length > 0) {
103
- output += "### Data Loss Analysis\n\n";
104
- for (const issue of dataLossIssues) {
105
- output += `**${issue.risk}**: ${issue.description}\n`;
106
- output += `> \`${issue.statement}\`\n`;
107
- output += `> **Mitigation**: ${issue.mitigation}\n\n`;
76
+ output += `**Description**: ${migration.description}\n`;
77
+ output += `**Statements**: ${migration.statements.length}\n`;
78
+ output += `**Risk Score**: ${riskScore}/100${riskScore >= 60 ? " ⚠️ HIGH RISK" : riskScore >= 30 ? " ⚡ MODERATE RISK" : " ✅ LOW RISK"}\n\n`;
79
+ // Statement summary
80
+ output += "### Operations\n\n";
81
+ const typeCounts = {};
82
+ for (const stmt of migration.statements) {
83
+ typeCounts[stmt.type] = (typeCounts[stmt.type] || 0) + 1;
84
+ }
85
+ output += "| Operation | Count |\n|-----------|-------|\n";
86
+ for (const [type, count] of Object.entries(typeCounts)) {
87
+ output += `| ${type} | ${count} |\n`;
88
+ }
89
+ output += "\n";
90
+ // Lock risks
91
+ if (lockRisks.length > 0) {
92
+ output += "### Lock Risks\n\n";
93
+ for (const risk of lockRisks) {
94
+ output += `**${risk.severity}**: ${risk.risk}\n`;
95
+ output += `> \`${risk.statement}\`\n`;
96
+ output += `> **Recommendation**: ${risk.recommendation}\n\n`;
97
+ }
108
98
  }
99
+ else {
100
+ output += "### Lock Risks\n\nNo lock risks detected.\n\n";
101
+ }
102
+ // Data loss issues
103
+ if (dataLossIssues.length > 0) {
104
+ output += "### Data Loss Analysis\n\n";
105
+ for (const issue of dataLossIssues) {
106
+ output += `**${issue.risk}**: ${issue.description}\n`;
107
+ output += `> \`${issue.statement}\`\n`;
108
+ output += `> **Mitigation**: ${issue.mitigation}\n\n`;
109
+ }
110
+ }
111
+ else {
112
+ output += "### Data Loss Analysis\n\nNo data loss risks detected.\n\n";
113
+ }
114
+ output += formatParserWarnings(migration);
115
+ return {
116
+ content: [{ type: "text", text: output }],
117
+ };
109
118
  }
110
- else {
111
- output += "### Data Loss Analysis\n\nNo data loss risks detected.\n\n";
119
+ catch (err) {
120
+ return {
121
+ content: [{
122
+ type: "text",
123
+ text: `Error analyzing migration: ${err instanceof Error ? err.message : String(err)}`,
124
+ }],
125
+ };
112
126
  }
113
- output += formatParserWarnings(migration);
114
- return {
115
- content: [{ type: "text", text: output }],
116
- };
117
127
  });
118
128
  // Tool 2: analyze_liquibase
119
129
  server.tool("analyze_liquibase", "Analyze a Liquibase XML changelog for lock risks, data loss potential, and unsafe patterns. Supports createTable, dropTable, addColumn, dropColumn, modifyDataType, createIndex, addForeignKeyConstraint, renameTable, renameColumn, and more.", {
120
130
  xml: z.string().describe("The Liquibase XML changelog content"),
121
131
  }, async ({ xml }) => {
122
- const migration = parseLiquibaseXml(xml);
123
- const lockRisks = analyzeLockRisks(migration);
124
- const dataLossIssues = analyzeDataLoss(migration);
125
- const riskScore = calculateRiskScore(lockRisks);
126
- let output = `## Liquibase Changelog Analysis\n\n`;
127
- output += `**ChangeSets**: ${migration.description}\n`;
128
- output += `**Statements**: ${migration.statements.length}\n`;
129
- output += `**Risk Score**: ${riskScore}/100${riskScore >= 60 ? " HIGH RISK" : riskScore >= 30 ? " MODERATE RISK" : " LOW RISK"}\n\n`;
130
- const typeCounts = {};
131
- for (const stmt of migration.statements) {
132
- typeCounts[stmt.type] = (typeCounts[stmt.type] || 0) + 1;
133
- }
134
- output += "### Operations\n\n| Operation | Count |\n|-----------|-------|\n";
135
- for (const [type, count] of Object.entries(typeCounts)) {
136
- output += `| ${type} | ${count} |\n`;
137
- }
138
- output += "\n";
139
- if (lockRisks.length > 0) {
140
- output += "### Lock Risks\n\n";
141
- for (const risk of lockRisks) {
142
- output += `**${risk.severity}**: ${risk.risk}\n> **Recommendation**: ${risk.recommendation}\n\n`;
132
+ try {
133
+ const migration = parseLiquibaseXml(xml);
134
+ const lockRisks = analyzeLockRisks(migration);
135
+ const dataLossIssues = analyzeDataLoss(migration);
136
+ const lockScore = calculateRiskScore(lockRisks);
137
+ const dataLossScore = Math.min(100, dataLossIssues.filter(i => i.risk === "CERTAIN").length * 25 +
138
+ dataLossIssues.filter(i => i.risk === "LIKELY").length * 15 +
139
+ dataLossIssues.filter(i => i.risk === "POSSIBLE").length * 5);
140
+ const riskScore = Math.min(100, lockScore + dataLossScore);
141
+ let output = `## Liquibase Changelog Analysis\n\n`;
142
+ output += `**ChangeSets**: ${migration.description}\n`;
143
+ output += `**Statements**: ${migration.statements.length}\n`;
144
+ output += `**Risk Score**: ${riskScore}/100${riskScore >= 60 ? " HIGH RISK" : riskScore >= 30 ? " MODERATE RISK" : " LOW RISK"}\n\n`;
145
+ const typeCounts = {};
146
+ for (const stmt of migration.statements) {
147
+ typeCounts[stmt.type] = (typeCounts[stmt.type] || 0) + 1;
143
148
  }
144
- }
145
- if (dataLossIssues.length > 0) {
146
- output += "### Data Loss Analysis\n\n";
147
- for (const issue of dataLossIssues) {
148
- output += `**${issue.risk}**: ${issue.description}\n> **Mitigation**: ${issue.mitigation}\n\n`;
149
+ output += "### Operations\n\n| Operation | Count |\n|-----------|-------|\n";
150
+ for (const [type, count] of Object.entries(typeCounts)) {
151
+ output += `| ${type} | ${count} |\n`;
152
+ }
153
+ output += "\n";
154
+ if (lockRisks.length > 0) {
155
+ output += "### Lock Risks\n\n";
156
+ for (const risk of lockRisks) {
157
+ output += `**${risk.severity}**: ${risk.risk}\n> **Recommendation**: ${risk.recommendation}\n\n`;
158
+ }
159
+ }
160
+ if (dataLossIssues.length > 0) {
161
+ output += "### Data Loss Analysis\n\n";
162
+ for (const issue of dataLossIssues) {
163
+ output += `**${issue.risk}**: ${issue.description}\n> **Mitigation**: ${issue.mitigation}\n\n`;
164
+ }
165
+ }
166
+ if (lockRisks.length === 0 && dataLossIssues.length === 0) {
167
+ output += "### No risks detected.\n";
149
168
  }
169
+ output += formatParserWarnings(migration);
170
+ return { content: [{ type: "text", text: output }] };
150
171
  }
151
- if (lockRisks.length === 0 && dataLossIssues.length === 0) {
152
- output += "### No risks detected.\n";
172
+ catch (err) {
173
+ return {
174
+ content: [{
175
+ type: "text",
176
+ text: `Error analyzing Liquibase changelog: ${err instanceof Error ? err.message : String(err)}`,
177
+ }],
178
+ };
153
179
  }
154
- output += formatParserWarnings(migration);
155
- return { content: [{ type: "text", text: output }] };
156
180
  });
157
181
  // Tool 3: analyze_liquibase_yaml
158
182
  server.tool("analyze_liquibase_yaml", "Analyze a Liquibase YAML changelog for lock risks, data loss potential, and unsafe patterns. Supports createTable, dropTable, addColumn, dropColumn, modifyDataType, createIndex, renameTable, renameColumn, and more.", {
159
183
  yaml: z.string().describe("The Liquibase YAML changelog content"),
160
184
  }, async ({ yaml }) => {
161
- const migration = parseLiquibaseYaml(yaml);
162
- const lockRisks = analyzeLockRisks(migration);
163
- const dataLossIssues = analyzeDataLoss(migration);
164
- const riskScore = calculateRiskScore(lockRisks);
165
- let output = `## Liquibase YAML Changelog Analysis\n\n`;
166
- output += `**ChangeSets**: ${migration.description}\n`;
167
- output += `**Statements**: ${migration.statements.length}\n`;
168
- output += `**Risk Score**: ${riskScore}/100${riskScore >= 60 ? " HIGH RISK" : riskScore >= 30 ? " MODERATE RISK" : " LOW RISK"}\n\n`;
169
- const typeCounts = {};
170
- for (const stmt of migration.statements) {
171
- typeCounts[stmt.type] = (typeCounts[stmt.type] || 0) + 1;
172
- }
173
- output += "### Operations\n\n| Operation | Count |\n|-----------|-------|\n";
174
- for (const [type, count] of Object.entries(typeCounts)) {
175
- output += `| ${type} | ${count} |\n`;
176
- }
177
- output += "\n";
178
- if (lockRisks.length > 0) {
179
- output += "### Lock Risks\n\n";
180
- for (const risk of lockRisks) {
181
- output += `**${risk.severity}**: ${risk.risk}\n> **Recommendation**: ${risk.recommendation}\n\n`;
185
+ try {
186
+ const migration = parseLiquibaseYaml(yaml);
187
+ const lockRisks = analyzeLockRisks(migration);
188
+ const dataLossIssues = analyzeDataLoss(migration);
189
+ const lockScore = calculateRiskScore(lockRisks);
190
+ const dataLossScore = Math.min(100, dataLossIssues.filter(i => i.risk === "CERTAIN").length * 25 +
191
+ dataLossIssues.filter(i => i.risk === "LIKELY").length * 15 +
192
+ dataLossIssues.filter(i => i.risk === "POSSIBLE").length * 5);
193
+ const riskScore = Math.min(100, lockScore + dataLossScore);
194
+ let output = `## Liquibase YAML Changelog Analysis\n\n`;
195
+ output += `**ChangeSets**: ${migration.description}\n`;
196
+ output += `**Statements**: ${migration.statements.length}\n`;
197
+ output += `**Risk Score**: ${riskScore}/100${riskScore >= 60 ? " HIGH RISK" : riskScore >= 30 ? " MODERATE RISK" : " LOW RISK"}\n\n`;
198
+ const typeCounts = {};
199
+ for (const stmt of migration.statements) {
200
+ typeCounts[stmt.type] = (typeCounts[stmt.type] || 0) + 1;
182
201
  }
183
- }
184
- if (dataLossIssues.length > 0) {
185
- output += "### Data Loss Analysis\n\n";
186
- for (const issue of dataLossIssues) {
187
- output += `**${issue.risk}**: ${issue.description}\n> **Mitigation**: ${issue.mitigation}\n\n`;
202
+ output += "### Operations\n\n| Operation | Count |\n|-----------|-------|\n";
203
+ for (const [type, count] of Object.entries(typeCounts)) {
204
+ output += `| ${type} | ${count} |\n`;
205
+ }
206
+ output += "\n";
207
+ if (lockRisks.length > 0) {
208
+ output += "### Lock Risks\n\n";
209
+ for (const risk of lockRisks) {
210
+ output += `**${risk.severity}**: ${risk.risk}\n> **Recommendation**: ${risk.recommendation}\n\n`;
211
+ }
212
+ }
213
+ if (dataLossIssues.length > 0) {
214
+ output += "### Data Loss Analysis\n\n";
215
+ for (const issue of dataLossIssues) {
216
+ output += `**${issue.risk}**: ${issue.description}\n> **Mitigation**: ${issue.mitigation}\n\n`;
217
+ }
188
218
  }
219
+ if (lockRisks.length === 0 && dataLossIssues.length === 0) {
220
+ output += "### No risks detected.\n";
221
+ }
222
+ output += formatParserWarnings(migration);
223
+ return { content: [{ type: "text", text: output }] };
189
224
  }
190
- if (lockRisks.length === 0 && dataLossIssues.length === 0) {
191
- output += "### No risks detected.\n";
225
+ catch (err) {
226
+ return {
227
+ content: [{
228
+ type: "text",
229
+ text: `Error analyzing Liquibase YAML changelog: ${err instanceof Error ? err.message : String(err)}`,
230
+ }],
231
+ };
192
232
  }
193
- output += formatParserWarnings(migration);
194
- return { content: [{ type: "text", text: output }] };
195
233
  });
196
234
  // Tool 4: score_risk
197
235
  server.tool("score_risk", "Calculate the combined risk score (0-100) for a SQL migration. Aggregates both lock risk severity (ACCESS EXCLUSIVE, SHARE locks) and data loss potential (DROP, TRUNCATE, type changes) into a single score. Useful for CI gates and automated migration review pipelines.", {
198
236
  filename: z.string().describe("Migration filename"),
199
237
  sql: z.string().describe("The SQL content of the migration file"),
200
238
  }, async ({ filename, sql }) => {
201
- const migration = parseMigration(filename, sql);
202
- const lockRisks = analyzeLockRisks(migration);
203
- const dataLossIssues = analyzeDataLoss(migration);
204
- const riskScore = calculateRiskScore(lockRisks);
205
- const criticalCount = lockRisks.filter(r => r.severity === "CRITICAL").length;
206
- const highCount = lockRisks.filter(r => r.severity === "HIGH").length;
207
- const mediumCount = lockRisks.filter(r => r.severity === "MEDIUM").length;
208
- const lowCount = lockRisks.filter(r => r.severity === "LOW").length;
209
- const dataLossCertain = dataLossIssues.filter(i => i.risk === "CERTAIN").length;
210
- const dataLossLikely = dataLossIssues.filter(i => i.risk === "LIKELY").length;
211
- const dataLossPossible = dataLossIssues.filter(i => i.risk === "POSSIBLE").length;
212
- // Combine lock risk score with data loss severity for a complete picture
213
- const dataLossScore = Math.min(100, dataLossCertain * 25 + dataLossLikely * 15 + dataLossPossible * 5);
214
- const combinedScore = Math.min(100, riskScore + dataLossScore);
215
- let verdict;
216
- if (combinedScore >= 60 || dataLossCertain > 0) {
217
- verdict = "HIGH RISK requires careful review and testing before deployment";
218
- }
219
- else if (combinedScore >= 30 || dataLossLikely > 0) {
220
- verdict = "MODERATE RISK review lock duration and test on staging";
221
- }
222
- else {
223
- verdict = "LOW RISK — standard migration, proceed with normal deployment";
224
- }
225
- const output = `## Risk Score: ${combinedScore}/100
239
+ try {
240
+ const migration = parseMigration(filename, sql);
241
+ const lockRisks = analyzeLockRisks(migration);
242
+ const dataLossIssues = analyzeDataLoss(migration);
243
+ const riskScore = calculateRiskScore(lockRisks);
244
+ const criticalCount = lockRisks.filter(r => r.severity === "CRITICAL").length;
245
+ const highCount = lockRisks.filter(r => r.severity === "HIGH").length;
246
+ const mediumCount = lockRisks.filter(r => r.severity === "MEDIUM").length;
247
+ const lowCount = lockRisks.filter(r => r.severity === "LOW").length;
248
+ const dataLossCertain = dataLossIssues.filter(i => i.risk === "CERTAIN").length;
249
+ const dataLossLikely = dataLossIssues.filter(i => i.risk === "LIKELY").length;
250
+ const dataLossPossible = dataLossIssues.filter(i => i.risk === "POSSIBLE").length;
251
+ // Combine lock risk score with data loss severity for a complete picture
252
+ const dataLossScore = Math.min(100, dataLossCertain * 25 + dataLossLikely * 15 + dataLossPossible * 5);
253
+ const combinedScore = Math.min(100, riskScore + dataLossScore);
254
+ let verdict;
255
+ if (combinedScore >= 60 || dataLossCertain > 0) {
256
+ verdict = "HIGH RISK — requires careful review and testing before deployment";
257
+ }
258
+ else if (combinedScore >= 30 || dataLossLikely > 0) {
259
+ verdict = "MODERATE RISK — review lock duration and test on staging";
260
+ }
261
+ else {
262
+ verdict = "LOW RISK — standard migration, proceed with normal deployment";
263
+ }
264
+ const output = `## Risk Score: ${combinedScore}/100
226
265
 
227
266
  **Verdict**: ${verdict}
228
267
 
@@ -239,33 +278,52 @@ server.tool("score_risk", "Calculate the combined risk score (0-100) for a SQL m
239
278
  | Possible data loss | ${dataLossPossible} | ${dataLossPossible * 5} |
240
279
  | Total statements | ${migration.statements.length} | — |
241
280
  `;
242
- return {
243
- content: [{ type: "text", text: output }],
244
- };
281
+ return {
282
+ content: [{ type: "text", text: output }],
283
+ };
284
+ }
285
+ catch (err) {
286
+ return {
287
+ content: [{
288
+ type: "text",
289
+ text: `Error scoring migration: ${err instanceof Error ? err.message : String(err)}`,
290
+ }],
291
+ };
292
+ }
245
293
  });
246
294
  // Tool 5: generate_rollback
247
295
  server.tool("generate_rollback", "Generate reverse DDL to undo a SQL migration. Produces rollback SQL with warnings for irreversible operations (DROP TABLE, DROP COLUMN, type changes). Includes Flyway schema_history cleanup.", {
248
296
  filename: z.string().describe("Migration filename (e.g., V2__add_user_email.sql)"),
249
297
  sql: z.string().describe("The SQL content of the migration file"),
250
298
  }, async ({ filename, sql }) => {
251
- const migration = parseMigration(filename, sql);
252
- const report = generateRollback(migration);
253
- let output = `## Rollback Script: ${filename}\n\n`;
254
- output += `**Reversible**: ${report.fullyReversible ? "Yes — all operations can be automatically reversed" : "Partially — some operations require manual intervention"}\n`;
255
- output += `**Statements**: ${report.statements.length}\n\n`;
256
- if (report.warnings.length > 0) {
257
- output += "### Warnings\n\n";
258
- for (const w of report.warnings) {
259
- output += `- ${w}\n`;
299
+ try {
300
+ const migration = parseMigration(filename, sql);
301
+ const report = generateRollback(migration);
302
+ let output = `## Rollback Script: ${filename}\n\n`;
303
+ output += `**Reversible**: ${report.fullyReversible ? "Yes — all operations can be automatically reversed" : "Partially — some operations require manual intervention"}\n`;
304
+ output += `**Statements**: ${report.statements.length}\n\n`;
305
+ if (report.warnings.length > 0) {
306
+ output += "### Warnings\n\n";
307
+ for (const w of report.warnings) {
308
+ output += `- ${w}\n`;
309
+ }
310
+ output += "\n";
260
311
  }
261
- output += "\n";
312
+ output += "### Rollback SQL\n\n```sql\n";
313
+ output += report.rollbackSql;
314
+ output += "\n```\n";
315
+ return { content: [{ type: "text", text: output }] };
316
+ }
317
+ catch (err) {
318
+ return {
319
+ content: [{
320
+ type: "text",
321
+ text: `Error generating rollback: ${err instanceof Error ? err.message : String(err)}`,
322
+ }],
323
+ };
262
324
  }
263
- output += "### Rollback SQL\n\n```sql\n";
264
- output += report.rollbackSql;
265
- output += "\n```\n";
266
- return { content: [{ type: "text", text: output }] };
267
325
  });
268
- // Tool 5: detect_conflicts
326
+ // Tool 6: detect_conflicts
269
327
  server.tool("detect_conflicts", "Detect conflicts between two SQL migration files. Identifies same-table modifications, same-column changes, lock contention risks, and drop dependencies that could cause failures if applied concurrently or in the wrong order.", {
270
328
  filename_a: z.string().describe("First migration filename (e.g., V3__add_email.sql)"),
271
329
  sql_a: z.string().describe("SQL content of the first migration"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-migration-advisor",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "MCP server for database migration risk analysis — Flyway and Liquibase XML/YAML/SQL support with lock detection and conflict analysis",
5
5
  "mcpName": "io.github.dmitriusan/mcp-migration-advisor",
6
6
  "main": "build/index.js",