mcp-migration-advisor 0.2.9 → 0.2.11
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 +1 -1
- package/build/analyzers/data-loss.js +6 -2
- package/build/index.js +223 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Unlike MigrationPilot (PG-only, raw SQL analysis), this tool **parses Liquibase
|
|
|
31
31
|
- Priority support
|
|
32
32
|
|
|
33
33
|
<!-- TODO: replace placeholder Stripe Payment Link once STRIPE_SECRET_KEY is configured -->
|
|
34
|
-
**$9.
|
|
34
|
+
**$9.00/month** — [Get Pro License](https://buy.stripe.com/PLACEHOLDER)
|
|
35
35
|
|
|
36
36
|
Pro license key activates the `generate_report` MCP tool in mcp-jvm-diagnostics.
|
|
37
37
|
|
|
@@ -101,8 +101,12 @@ export function analyzeDataLoss(migration) {
|
|
|
101
101
|
mitigation: "Add a WHERE clause to limit the delete, or use TRUNCATE if intentional.",
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
-
// Detect UPDATE without WHERE
|
|
105
|
-
|
|
104
|
+
// Detect UPDATE without WHERE.
|
|
105
|
+
// Match UPDATE at the start of the string or after a statement separator
|
|
106
|
+
// (semicolon or newline), allowing leading whitespace on the line.
|
|
107
|
+
// This ensures detection works in multi-statement <sql> Liquibase blocks
|
|
108
|
+
// where UPDATE is not the very first statement.
|
|
109
|
+
if (upper.match(/(?:^|[;\n])\s*UPDATE\b/) && !upper.includes("WHERE")) {
|
|
106
110
|
const tableMatch = stmt.raw.match(/UPDATE\s+(?:`|"|)?(?:\w+\.)?(\w+)/i);
|
|
107
111
|
issues.push({
|
|
108
112
|
risk: "LIKELY",
|
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,177 +52,224 @@ 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
|
-
dataLossIssues.filter(i => i.risk === "
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
99
|
-
output += "
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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`;
|
|
158
|
+
output += `> \`${risk.statement}\`\n`;
|
|
159
|
+
output += `> **Recommendation**: ${risk.recommendation}\n\n`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (dataLossIssues.length > 0) {
|
|
163
|
+
output += "### Data Loss Analysis\n\n";
|
|
164
|
+
for (const issue of dataLossIssues) {
|
|
165
|
+
output += `**${issue.risk}**: ${issue.description}\n`;
|
|
166
|
+
output += `> \`${issue.statement}\`\n`;
|
|
167
|
+
output += `> **Mitigation**: ${issue.mitigation}\n\n`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (lockRisks.length === 0 && dataLossIssues.length === 0) {
|
|
171
|
+
output += "### No risks detected.\n";
|
|
149
172
|
}
|
|
173
|
+
output += formatParserWarnings(migration);
|
|
174
|
+
return { content: [{ type: "text", text: output }] };
|
|
150
175
|
}
|
|
151
|
-
|
|
152
|
-
|
|
176
|
+
catch (err) {
|
|
177
|
+
return {
|
|
178
|
+
content: [{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: `Error analyzing Liquibase changelog: ${err instanceof Error ? err.message : String(err)}`,
|
|
181
|
+
}],
|
|
182
|
+
};
|
|
153
183
|
}
|
|
154
|
-
output += formatParserWarnings(migration);
|
|
155
|
-
return { content: [{ type: "text", text: output }] };
|
|
156
184
|
});
|
|
157
185
|
// Tool 3: analyze_liquibase_yaml
|
|
158
|
-
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.", {
|
|
186
|
+
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, addForeignKeyConstraint, renameTable, renameColumn, and more.", {
|
|
159
187
|
yaml: z.string().describe("The Liquibase YAML changelog content"),
|
|
160
188
|
}, async ({ yaml }) => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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`;
|
|
189
|
+
try {
|
|
190
|
+
const migration = parseLiquibaseYaml(yaml);
|
|
191
|
+
const lockRisks = analyzeLockRisks(migration);
|
|
192
|
+
const dataLossIssues = analyzeDataLoss(migration);
|
|
193
|
+
const lockScore = calculateRiskScore(lockRisks);
|
|
194
|
+
const dataLossScore = Math.min(100, dataLossIssues.filter(i => i.risk === "CERTAIN").length * 25 +
|
|
195
|
+
dataLossIssues.filter(i => i.risk === "LIKELY").length * 15 +
|
|
196
|
+
dataLossIssues.filter(i => i.risk === "POSSIBLE").length * 5);
|
|
197
|
+
const riskScore = Math.min(100, lockScore + dataLossScore);
|
|
198
|
+
let output = `## Liquibase YAML Changelog Analysis\n\n`;
|
|
199
|
+
output += `**ChangeSets**: ${migration.description}\n`;
|
|
200
|
+
output += `**Statements**: ${migration.statements.length}\n`;
|
|
201
|
+
output += `**Risk Score**: ${riskScore}/100${riskScore >= 60 ? " ⚠️ HIGH RISK" : riskScore >= 30 ? " ⚡ MODERATE RISK" : " ✅ LOW RISK"}\n\n`;
|
|
202
|
+
const typeCounts = {};
|
|
203
|
+
for (const stmt of migration.statements) {
|
|
204
|
+
typeCounts[stmt.type] = (typeCounts[stmt.type] || 0) + 1;
|
|
182
205
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
206
|
+
output += "### Operations\n\n| Operation | Count |\n|-----------|-------|\n";
|
|
207
|
+
for (const [type, count] of Object.entries(typeCounts)) {
|
|
208
|
+
output += `| ${type} | ${count} |\n`;
|
|
209
|
+
}
|
|
210
|
+
output += "\n";
|
|
211
|
+
if (lockRisks.length > 0) {
|
|
212
|
+
output += "### Lock Risks\n\n";
|
|
213
|
+
for (const risk of lockRisks) {
|
|
214
|
+
output += `**${risk.severity}**: ${risk.risk}\n`;
|
|
215
|
+
output += `> \`${risk.statement}\`\n`;
|
|
216
|
+
output += `> **Recommendation**: ${risk.recommendation}\n\n`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (dataLossIssues.length > 0) {
|
|
220
|
+
output += "### Data Loss Analysis\n\n";
|
|
221
|
+
for (const issue of dataLossIssues) {
|
|
222
|
+
output += `**${issue.risk}**: ${issue.description}\n`;
|
|
223
|
+
output += `> \`${issue.statement}\`\n`;
|
|
224
|
+
output += `> **Mitigation**: ${issue.mitigation}\n\n`;
|
|
225
|
+
}
|
|
188
226
|
}
|
|
227
|
+
if (lockRisks.length === 0 && dataLossIssues.length === 0) {
|
|
228
|
+
output += "### No risks detected.\n";
|
|
229
|
+
}
|
|
230
|
+
output += formatParserWarnings(migration);
|
|
231
|
+
return { content: [{ type: "text", text: output }] };
|
|
189
232
|
}
|
|
190
|
-
|
|
191
|
-
|
|
233
|
+
catch (err) {
|
|
234
|
+
return {
|
|
235
|
+
content: [{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: `Error analyzing Liquibase YAML changelog: ${err instanceof Error ? err.message : String(err)}`,
|
|
238
|
+
}],
|
|
239
|
+
};
|
|
192
240
|
}
|
|
193
|
-
output += formatParserWarnings(migration);
|
|
194
|
-
return { content: [{ type: "text", text: output }] };
|
|
195
241
|
});
|
|
196
242
|
// Tool 4: score_risk
|
|
197
243
|
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
244
|
filename: z.string().describe("Migration filename"),
|
|
199
245
|
sql: z.string().describe("The SQL content of the migration file"),
|
|
200
246
|
}, async ({ filename, sql }) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
247
|
+
try {
|
|
248
|
+
const migration = parseMigration(filename, sql);
|
|
249
|
+
const lockRisks = analyzeLockRisks(migration);
|
|
250
|
+
const dataLossIssues = analyzeDataLoss(migration);
|
|
251
|
+
const riskScore = calculateRiskScore(lockRisks);
|
|
252
|
+
const criticalCount = lockRisks.filter(r => r.severity === "CRITICAL").length;
|
|
253
|
+
const highCount = lockRisks.filter(r => r.severity === "HIGH").length;
|
|
254
|
+
const mediumCount = lockRisks.filter(r => r.severity === "MEDIUM").length;
|
|
255
|
+
const lowCount = lockRisks.filter(r => r.severity === "LOW").length;
|
|
256
|
+
const dataLossCertain = dataLossIssues.filter(i => i.risk === "CERTAIN").length;
|
|
257
|
+
const dataLossLikely = dataLossIssues.filter(i => i.risk === "LIKELY").length;
|
|
258
|
+
const dataLossPossible = dataLossIssues.filter(i => i.risk === "POSSIBLE").length;
|
|
259
|
+
// Combine lock risk score with data loss severity for a complete picture
|
|
260
|
+
const dataLossScore = Math.min(100, dataLossCertain * 25 + dataLossLikely * 15 + dataLossPossible * 5);
|
|
261
|
+
const combinedScore = Math.min(100, riskScore + dataLossScore);
|
|
262
|
+
let verdict;
|
|
263
|
+
if (combinedScore >= 60 || dataLossCertain > 0) {
|
|
264
|
+
verdict = "HIGH RISK — requires careful review and testing before deployment";
|
|
265
|
+
}
|
|
266
|
+
else if (combinedScore >= 30 || dataLossLikely > 0) {
|
|
267
|
+
verdict = "MODERATE RISK — review lock duration and test on staging";
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
verdict = "LOW RISK — standard migration, proceed with normal deployment";
|
|
271
|
+
}
|
|
272
|
+
const output = `## Risk Score: ${combinedScore}/100
|
|
226
273
|
|
|
227
274
|
**Verdict**: ${verdict}
|
|
228
275
|
|
|
@@ -239,33 +286,52 @@ server.tool("score_risk", "Calculate the combined risk score (0-100) for a SQL m
|
|
|
239
286
|
| Possible data loss | ${dataLossPossible} | ${dataLossPossible * 5} |
|
|
240
287
|
| Total statements | ${migration.statements.length} | — |
|
|
241
288
|
`;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: "text", text: output }],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
return {
|
|
295
|
+
content: [{
|
|
296
|
+
type: "text",
|
|
297
|
+
text: `Error scoring migration: ${err instanceof Error ? err.message : String(err)}`,
|
|
298
|
+
}],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
245
301
|
});
|
|
246
302
|
// Tool 5: generate_rollback
|
|
247
303
|
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
304
|
filename: z.string().describe("Migration filename (e.g., V2__add_user_email.sql)"),
|
|
249
305
|
sql: z.string().describe("The SQL content of the migration file"),
|
|
250
306
|
}, async ({ filename, sql }) => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
307
|
+
try {
|
|
308
|
+
const migration = parseMigration(filename, sql);
|
|
309
|
+
const report = generateRollback(migration);
|
|
310
|
+
let output = `## Rollback Script: ${filename}\n\n`;
|
|
311
|
+
output += `**Reversible**: ${report.fullyReversible ? "Yes — all operations can be automatically reversed" : "Partially — some operations require manual intervention"}\n`;
|
|
312
|
+
output += `**Statements**: ${report.statements.length}\n\n`;
|
|
313
|
+
if (report.warnings.length > 0) {
|
|
314
|
+
output += "### Warnings\n\n";
|
|
315
|
+
for (const w of report.warnings) {
|
|
316
|
+
output += `- ${w}\n`;
|
|
317
|
+
}
|
|
318
|
+
output += "\n";
|
|
260
319
|
}
|
|
261
|
-
output += "\n";
|
|
320
|
+
output += "### Rollback SQL\n\n```sql\n";
|
|
321
|
+
output += report.rollbackSql;
|
|
322
|
+
output += "\n```\n";
|
|
323
|
+
return { content: [{ type: "text", text: output }] };
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
return {
|
|
327
|
+
content: [{
|
|
328
|
+
type: "text",
|
|
329
|
+
text: `Error generating rollback: ${err instanceof Error ? err.message : String(err)}`,
|
|
330
|
+
}],
|
|
331
|
+
};
|
|
262
332
|
}
|
|
263
|
-
output += "### Rollback SQL\n\n```sql\n";
|
|
264
|
-
output += report.rollbackSql;
|
|
265
|
-
output += "\n```\n";
|
|
266
|
-
return { content: [{ type: "text", text: output }] };
|
|
267
333
|
});
|
|
268
|
-
// Tool
|
|
334
|
+
// Tool 6: detect_conflicts
|
|
269
335
|
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
336
|
filename_a: z.string().describe("First migration filename (e.g., V3__add_email.sql)"),
|
|
271
337
|
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.11",
|
|
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",
|