mcp-migration-advisor 0.2.0 → 0.2.3

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/index.js CHANGED
@@ -16,9 +16,21 @@ import { analyzeLockRisks, calculateRiskScore } from "./analyzers/lock-risk.js";
16
16
  import { analyzeDataLoss } from "./analyzers/data-loss.js";
17
17
  import { generateRollback } from "./generators/rollback.js";
18
18
  import { detectConflicts, formatConflictReport } from "./analyzers/conflicts.js";
19
- import { validateLicense, formatUpgradePrompt } from "./license.js";
20
- // License check (reads MCP_LICENSE_KEY env var once at startup)
21
- const license = validateLicense(process.env.MCP_LICENSE_KEY, "migration-advisor");
19
+ /**
20
+ * Format parser warnings into a markdown section.
21
+ * Returns empty string if there are no warnings.
22
+ */
23
+ function formatParserWarnings(migration) {
24
+ if (migration.warnings.length === 0)
25
+ return "";
26
+ let output = "### Parser Warnings\n\n";
27
+ output += `> ${migration.warnings.length} DDL statement(s) could not be fully parsed and were classified as OTHER\n\n`;
28
+ for (const w of migration.warnings) {
29
+ output += `- \`${w.snippet}\`\n`;
30
+ }
31
+ output += "\n";
32
+ return output;
33
+ }
22
34
  // Handle --help
23
35
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
24
36
  console.log(`mcp-migration-advisor v0.1.0 — MCP server for database migration risk analysis
@@ -94,6 +106,7 @@ server.tool("analyze_migration", "Analyze a SQL migration file for lock risks, d
94
106
  else {
95
107
  output += "### Data Loss Analysis\n\nNo data loss risks detected.\n\n";
96
108
  }
109
+ output += formatParserWarnings(migration);
97
110
  return {
98
111
  content: [{ type: "text", text: output }],
99
112
  };
@@ -134,6 +147,7 @@ server.tool("analyze_liquibase", "Analyze a Liquibase XML changelog for lock ris
134
147
  if (lockRisks.length === 0 && dataLossIssues.length === 0) {
135
148
  output += "### No risks detected.\n";
136
149
  }
150
+ output += formatParserWarnings(migration);
137
151
  return { content: [{ type: "text", text: output }] };
138
152
  });
139
153
  // Tool 3: analyze_liquibase_yaml
@@ -172,6 +186,7 @@ server.tool("analyze_liquibase_yaml", "Analyze a Liquibase YAML changelog for lo
172
186
  if (lockRisks.length === 0 && dataLossIssues.length === 0) {
173
187
  output += "### No risks detected.\n";
174
188
  }
189
+ output += formatParserWarnings(migration);
175
190
  return { content: [{ type: "text", text: output }] };
176
191
  });
177
192
  // Tool 4: score_risk
@@ -220,21 +235,6 @@ server.tool("generate_rollback", "Generate reverse DDL to undo a SQL migration.
220
235
  filename: z.string().describe("Migration filename (e.g., V2__add_user_email.sql)"),
221
236
  sql: z.string().describe("The SQL content of the migration file"),
222
237
  }, async ({ filename, sql }) => {
223
- // Pro feature gate — free users get a preview, Pro users get full output
224
- if (!license.isPro) {
225
- const migration = parseMigration(filename, sql);
226
- const report = generateRollback(migration);
227
- // Show a preview: statement count and reversibility, but not the actual SQL
228
- let preview = `## Rollback Preview: ${filename}\n\n`;
229
- preview += `**Reversible**: ${report.fullyReversible ? "Yes" : "Partially"}\n`;
230
- preview += `**Statements**: ${report.statements.length}\n`;
231
- preview += `**Warnings**: ${report.warnings.length}\n\n`;
232
- preview += formatUpgradePrompt("generate_rollback", "Full rollback SQL generation with:\n" +
233
- "- Complete reverse DDL for all migration operations\n" +
234
- "- Flyway schema_history cleanup statements\n" +
235
- "- Irreversibility warnings with manual intervention guidance");
236
- return { content: [{ type: "text", text: preview }] };
237
- }
238
238
  const migration = parseMigration(filename, sql);
239
239
  const report = generateRollback(migration);
240
240
  let output = `## Rollback Script: ${filename}\n\n`;
@@ -171,6 +171,30 @@ function classifyStatement(raw) {
171
171
  }
172
172
  return { type: "OTHER", raw, tableName: null, columnName: null, details };
173
173
  }
174
+ /**
175
+ * Build a snippet of up to ~80 characters from a raw statement for debugging.
176
+ */
177
+ export function truncateSnippet(raw, maxLen = 80) {
178
+ const oneLine = raw.replace(/\s+/g, " ").trim();
179
+ if (oneLine.length <= maxLen)
180
+ return oneLine;
181
+ return oneLine.substring(0, maxLen) + "...";
182
+ }
183
+ /**
184
+ * Collect warnings for statements classified as OTHER.
185
+ */
186
+ export function collectParserWarnings(statements) {
187
+ const warnings = [];
188
+ for (const stmt of statements) {
189
+ if (stmt.type === "OTHER") {
190
+ warnings.push({
191
+ message: "Unrecognized DDL statement classified as OTHER",
192
+ snippet: truncateSnippet(stmt.raw),
193
+ });
194
+ }
195
+ }
196
+ return warnings;
197
+ }
174
198
  /**
175
199
  * Parse a complete Flyway migration file.
176
200
  */
@@ -178,12 +202,14 @@ export function parseMigration(filename, sql) {
178
202
  const { version, description, isRepeatable } = parseFlywayFilename(filename);
179
203
  const rawStatements = splitStatements(sql);
180
204
  const statements = rawStatements.map(classifyStatement);
205
+ const warnings = collectParserWarnings(statements);
181
206
  return {
182
207
  version,
183
208
  description,
184
209
  filename,
185
210
  isRepeatable,
186
211
  statements,
212
+ warnings,
187
213
  };
188
214
  }
189
215
  //# sourceMappingURL=flyway-sql.js.map
@@ -4,6 +4,7 @@
4
4
  * Parses Liquibase XML changelogs and extracts DDL operations
5
5
  * compatible with the same DDLStatement interface used by Flyway.
6
6
  */
7
+ import { collectParserWarnings } from "./flyway-sql.js";
7
8
  /**
8
9
  * Parse a Liquibase XML changelog and extract DDL statements.
9
10
  *
@@ -23,6 +24,7 @@ export function parseLiquibaseXml(xml) {
23
24
  filename: "changelog.xml",
24
25
  isRepeatable: false,
25
26
  statements: allStatements,
27
+ warnings: collectParserWarnings(allStatements),
26
28
  };
27
29
  }
28
30
  function extractChangeSets(xml) {
@@ -4,6 +4,7 @@
4
4
  * Parses Liquibase YAML changelogs and extracts DDL operations
5
5
  * compatible with the same DDLStatement interface used by Flyway and XML.
6
6
  */
7
+ import { collectParserWarnings } from "./flyway-sql.js";
7
8
  /**
8
9
  * Parse a Liquibase YAML changelog and extract DDL statements.
9
10
  *
@@ -23,6 +24,7 @@ export function parseLiquibaseYaml(yaml) {
23
24
  filename: "changelog.yaml",
24
25
  isRepeatable: false,
25
26
  statements: allStatements,
27
+ warnings: collectParserWarnings(allStatements),
26
28
  };
27
29
  }
28
30
  function extractChangeSets(yaml) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-migration-advisor",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
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
  "main": "build/index.js",
6
6
  "bin": {
@@ -20,17 +20,21 @@
20
20
  ],
21
21
  "keywords": [
22
22
  "mcp",
23
+ "mcp-server",
23
24
  "model-context-protocol",
24
- "database",
25
+ "ai",
26
+ "claude",
27
+ "anthropic",
25
28
  "migration",
29
+ "database-migration",
30
+ "schema-migration",
31
+ "database",
26
32
  "flyway",
27
33
  "liquibase",
28
34
  "schema",
29
35
  "risk-analysis",
30
36
  "postgresql",
31
- "mysql",
32
- "ai",
33
- "claude"
37
+ "mysql"
34
38
  ],
35
39
  "author": "Dmytro Lisnichenko",
36
40
  "license": "MIT",
package/build/license.js DELETED
@@ -1,115 +0,0 @@
1
- /**
2
- * License validation for MCP Migration Advisor (Pro features).
3
- *
4
- * Validates license keys offline using HMAC-SHA256.
5
- * Missing or invalid keys gracefully degrade to free mode — never errors.
6
- *
7
- * Key format: MCPJBS-XXXXX-XXXXX-XXXXX-XXXXX
8
- * Payload (12 bytes = 20 base32 chars):
9
- * [0] product mask (8 bits)
10
- * [1-2] expiry days since 2026-01-01 (16 bits)
11
- * [3-5] customer ID (24 bits)
12
- * [6-11] HMAC-SHA256 truncated (48 bits)
13
- */
14
- import { createHmac } from "node:crypto";
15
- const KEY_PREFIX = "MCPJBS-";
16
- const EPOCH = new Date("2026-01-01T00:00:00Z");
17
- const BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
18
- const HMAC_SECRET = "mcp-java-backend-suite-license-v1";
19
- const PRODUCTS = {
20
- "db-analyzer": 0,
21
- "jvm-diagnostics": 1,
22
- "migration-advisor": 2,
23
- "spring-boot-actuator": 3,
24
- "redis-diagnostics": 4,
25
- };
26
- export function validateLicense(key, product) {
27
- const FREE = {
28
- isPro: false,
29
- expiresAt: null,
30
- customerId: null,
31
- reason: "No license key provided",
32
- };
33
- if (!key || key.trim().length === 0)
34
- return FREE;
35
- const trimmed = key.trim().toUpperCase();
36
- if (!trimmed.startsWith(KEY_PREFIX)) {
37
- return { ...FREE, reason: "Invalid key format: missing MCPJBS- prefix" };
38
- }
39
- const body = trimmed.slice(KEY_PREFIX.length).replace(/-/g, "");
40
- if (body.length < 20) {
41
- return { ...FREE, reason: "Invalid key format: too short" };
42
- }
43
- let decoded;
44
- try {
45
- decoded = base32Decode(body.slice(0, 20));
46
- }
47
- catch {
48
- return { ...FREE, reason: "Invalid key format: bad base32 encoding" };
49
- }
50
- if (decoded.length < 12) {
51
- return { ...FREE, reason: "Invalid key format: decoded data too short" };
52
- }
53
- const payload = decoded.subarray(0, 6);
54
- const providedSignature = decoded.subarray(6, 12);
55
- const expectedHmac = createHmac("sha256", HMAC_SECRET)
56
- .update(payload)
57
- .digest();
58
- const expectedSignature = expectedHmac.subarray(0, 6);
59
- if (!providedSignature.equals(expectedSignature)) {
60
- return { ...FREE, reason: "Invalid license key: signature mismatch" };
61
- }
62
- const productMask = payload[0];
63
- const daysSinceEpoch = (payload[1] << 8) | payload[2];
64
- const customerId = (payload[3] << 16) | (payload[4] << 8) | payload[5];
65
- const productBit = PRODUCTS[product];
66
- if (productBit === undefined) {
67
- return { ...FREE, reason: `Unknown product: ${product}` };
68
- }
69
- if ((productMask & (1 << productBit)) === 0) {
70
- return { ...FREE, customerId, reason: `License does not include ${product}` };
71
- }
72
- const expiresAt = new Date(EPOCH.getTime() + daysSinceEpoch * 24 * 60 * 60 * 1000);
73
- if (new Date() > expiresAt) {
74
- return {
75
- isPro: false,
76
- expiresAt,
77
- customerId,
78
- reason: `License expired on ${expiresAt.toISOString().slice(0, 10)}`,
79
- };
80
- }
81
- return { isPro: true, expiresAt, customerId, reason: "Valid Pro license" };
82
- }
83
- export function formatUpgradePrompt(toolName, featureDescription) {
84
- return [
85
- `## ${toolName} (Pro Feature)`,
86
- "",
87
- "This analysis is available with MCP Java Backend Suite Pro.",
88
- "",
89
- `**What you'll get:**`,
90
- featureDescription,
91
- "",
92
- "**Upgrade**: https://mcpjbs.dev/pricing",
93
- "**Price**: $19/month or $190/year",
94
- "",
95
- "> Already have a key? Set `MCP_LICENSE_KEY` in your Claude Desktop config.",
96
- ].join("\n");
97
- }
98
- function base32Decode(encoded) {
99
- const bytes = [];
100
- let bits = 0;
101
- let value = 0;
102
- for (const char of encoded) {
103
- const idx = BASE32_CHARS.indexOf(char);
104
- if (idx === -1)
105
- throw new Error(`Invalid base32 character: ${char}`);
106
- value = (value << 5) | idx;
107
- bits += 5;
108
- if (bits >= 8) {
109
- bits -= 8;
110
- bytes.push((value >> bits) & 0xff);
111
- }
112
- }
113
- return Buffer.from(bytes);
114
- }
115
- //# sourceMappingURL=license.js.map