mcp-migration-advisor 0.2.8 → 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.
- package/build/generators/rollback.js +6 -2
- package/build/index.js +214 -152
- package/package.json +1 -1
|
@@ -66,13 +66,17 @@ function generateRollbackStatement(stmt) {
|
|
|
66
66
|
isReversible: true,
|
|
67
67
|
warning: null,
|
|
68
68
|
};
|
|
69
|
-
case "DROP_TABLE":
|
|
69
|
+
case "DROP_TABLE": {
|
|
70
|
+
const hasCascade = stmt.details.cascade === "true";
|
|
70
71
|
return {
|
|
71
72
|
forward: stmt.raw,
|
|
72
73
|
rollback: `-- Cannot reverse DROP TABLE ${stmt.tableName} without schema backup`,
|
|
73
74
|
isReversible: false,
|
|
74
|
-
warning:
|
|
75
|
+
warning: hasCascade
|
|
76
|
+
? `DROP TABLE ${stmt.tableName} CASCADE is irreversible — table and all dependent objects (views, foreign keys, indexes) are permanently destroyed. Each dependent object must be recreated manually.`
|
|
77
|
+
: `DROP TABLE ${stmt.tableName} is irreversible — table structure and data are lost. Restore from backup.`,
|
|
75
78
|
};
|
|
79
|
+
}
|
|
76
80
|
case "CREATE_INDEX": {
|
|
77
81
|
// Extract index name from the raw SQL
|
|
78
82
|
const indexMatch = stmt.raw.match(/INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(?:`|"|)?(\w+)/i);
|
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.
|
|
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,173 +52,216 @@ Tools provided:
|
|
|
52
52
|
}
|
|
53
53
|
const server = new McpServer({
|
|
54
54
|
name: "mcp-migration-advisor",
|
|
55
|
-
version: "0.2.
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
output += "### Operations\n\n";
|
|
76
|
-
const typeCounts = {};
|
|
77
|
-
for (const stmt of migration.statements) {
|
|
78
|
-
typeCounts[stmt.type] = (typeCounts[stmt.type] || 0) + 1;
|
|
79
|
-
}
|
|
80
|
-
output += "| Operation | Count |\n|-----------|-------|\n";
|
|
81
|
-
for (const [type, count] of Object.entries(typeCounts)) {
|
|
82
|
-
output += `| ${type} | ${count} |\n`;
|
|
83
|
-
}
|
|
84
|
-
output += "\n";
|
|
85
|
-
// Lock risks
|
|
86
|
-
if (lockRisks.length > 0) {
|
|
87
|
-
output += "### Lock Risks\n\n";
|
|
88
|
-
for (const risk of lockRisks) {
|
|
89
|
-
output += `**${risk.severity}**: ${risk.risk}\n`;
|
|
90
|
-
output += `> \`${risk.statement}\`\n`;
|
|
91
|
-
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`;
|
|
92
75
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
output += "
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
}
|
|
104
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
|
+
};
|
|
105
118
|
}
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
catch (err) {
|
|
120
|
+
return {
|
|
121
|
+
content: [{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: `Error analyzing migration: ${err instanceof Error ? err.message : String(err)}`,
|
|
124
|
+
}],
|
|
125
|
+
};
|
|
108
126
|
}
|
|
109
|
-
output += formatParserWarnings(migration);
|
|
110
|
-
return {
|
|
111
|
-
content: [{ type: "text", text: output }],
|
|
112
|
-
};
|
|
113
127
|
});
|
|
114
128
|
// Tool 2: analyze_liquibase
|
|
115
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.", {
|
|
116
130
|
xml: z.string().describe("The Liquibase XML changelog content"),
|
|
117
131
|
}, async ({ xml }) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
output += "\n";
|
|
135
|
-
if (lockRisks.length > 0) {
|
|
136
|
-
output += "### Lock Risks\n\n";
|
|
137
|
-
for (const risk of lockRisks) {
|
|
138
|
-
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;
|
|
139
148
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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";
|
|
145
168
|
}
|
|
169
|
+
output += formatParserWarnings(migration);
|
|
170
|
+
return { content: [{ type: "text", text: output }] };
|
|
146
171
|
}
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
};
|
|
149
179
|
}
|
|
150
|
-
output += formatParserWarnings(migration);
|
|
151
|
-
return { content: [{ type: "text", text: output }] };
|
|
152
180
|
});
|
|
153
181
|
// Tool 3: analyze_liquibase_yaml
|
|
154
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.", {
|
|
155
183
|
yaml: z.string().describe("The Liquibase YAML changelog content"),
|
|
156
184
|
}, async ({ yaml }) => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
output += "\n";
|
|
174
|
-
if (lockRisks.length > 0) {
|
|
175
|
-
output += "### Lock Risks\n\n";
|
|
176
|
-
for (const risk of lockRisks) {
|
|
177
|
-
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;
|
|
178
201
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
}
|
|
184
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 }] };
|
|
185
224
|
}
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
};
|
|
188
232
|
}
|
|
189
|
-
output += formatParserWarnings(migration);
|
|
190
|
-
return { content: [{ type: "text", text: output }] };
|
|
191
233
|
});
|
|
192
234
|
// Tool 4: score_risk
|
|
193
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.", {
|
|
194
236
|
filename: z.string().describe("Migration filename"),
|
|
195
237
|
sql: z.string().describe("The SQL content of the migration file"),
|
|
196
238
|
}, async ({ filename, sql }) => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
222
265
|
|
|
223
266
|
**Verdict**: ${verdict}
|
|
224
267
|
|
|
@@ -235,33 +278,52 @@ server.tool("score_risk", "Calculate the combined risk score (0-100) for a SQL m
|
|
|
235
278
|
| Possible data loss | ${dataLossPossible} | ${dataLossPossible * 5} |
|
|
236
279
|
| Total statements | ${migration.statements.length} | — |
|
|
237
280
|
`;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|
|
241
293
|
});
|
|
242
294
|
// Tool 5: generate_rollback
|
|
243
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.", {
|
|
244
296
|
filename: z.string().describe("Migration filename (e.g., V2__add_user_email.sql)"),
|
|
245
297
|
sql: z.string().describe("The SQL content of the migration file"),
|
|
246
298
|
}, async ({ filename, sql }) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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";
|
|
256
311
|
}
|
|
257
|
-
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
|
+
};
|
|
258
324
|
}
|
|
259
|
-
output += "### Rollback SQL\n\n```sql\n";
|
|
260
|
-
output += report.rollbackSql;
|
|
261
|
-
output += "\n```\n";
|
|
262
|
-
return { content: [{ type: "text", text: output }] };
|
|
263
325
|
});
|
|
264
|
-
// Tool
|
|
326
|
+
// Tool 6: detect_conflicts
|
|
265
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.", {
|
|
266
328
|
filename_a: z.string().describe("First migration filename (e.g., V3__add_email.sql)"),
|
|
267
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.
|
|
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",
|