mcp-migration-advisor 0.2.2 → 0.2.4

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.
@@ -147,6 +147,17 @@ function analyzeStatement(stmt) {
147
147
  recommendation: "Avoid explicit locks in migrations. Use row-level locking or redesign the migration.",
148
148
  });
149
149
  }
150
+ // TRUNCATE acquires ACCESS EXCLUSIVE lock — same severity as DROP TABLE
151
+ if (stmt.type === "OTHER" && /\bTRUNCATE\b/i.test(stmt.raw)) {
152
+ const tableMatch = stmt.raw.match(/TRUNCATE\s+(?:TABLE\s+)?(?:`|"|)?(\w+)/i);
153
+ risks.push({
154
+ severity: "HIGH",
155
+ statement: truncate(stmt.raw),
156
+ tableName: tableMatch?.[1] || null,
157
+ risk: "TRUNCATE acquires ACCESS EXCLUSIVE lock, blocking all concurrent reads and writes for the duration.",
158
+ recommendation: "On large tables, prefer DELETE with a WHERE clause in batches. If speed is critical, ensure a maintenance window.",
159
+ });
160
+ }
150
161
  return risks;
151
162
  }
152
163
  function truncate(s, max = 120) {
package/build/index.js CHANGED
@@ -16,9 +16,6 @@ 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");
22
19
  /**
23
20
  * Format parser warnings into a markdown section.
24
21
  * Returns empty string if there are no warnings.
@@ -36,7 +33,7 @@ function formatParserWarnings(migration) {
36
33
  }
37
34
  // Handle --help
38
35
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
39
- console.log(`mcp-migration-advisor v0.1.0 — MCP server for database migration risk analysis
36
+ console.log(`mcp-migration-advisor v0.2.4 — MCP server for database migration risk analysis
40
37
 
41
38
  Usage:
42
39
  mcp-migration-advisor [options]
@@ -55,7 +52,7 @@ Tools provided:
55
52
  }
56
53
  const server = new McpServer({
57
54
  name: "mcp-migration-advisor",
58
- version: "0.1.0",
55
+ version: "0.2.4",
59
56
  });
60
57
  // Tool 1: analyze_migration
61
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.", {
@@ -238,21 +235,6 @@ server.tool("generate_rollback", "Generate reverse DDL to undo a SQL migration.
238
235
  filename: z.string().describe("Migration filename (e.g., V2__add_user_email.sql)"),
239
236
  sql: z.string().describe("The SQL content of the migration file"),
240
237
  }, async ({ filename, sql }) => {
241
- // Pro feature gate — free users get a preview, Pro users get full output
242
- if (!license.isPro) {
243
- const migration = parseMigration(filename, sql);
244
- const report = generateRollback(migration);
245
- // Show a preview: statement count and reversibility, but not the actual SQL
246
- let preview = `## Rollback Preview: ${filename}\n\n`;
247
- preview += `**Reversible**: ${report.fullyReversible ? "Yes" : "Partially"}\n`;
248
- preview += `**Statements**: ${report.statements.length}\n`;
249
- preview += `**Warnings**: ${report.warnings.length}\n\n`;
250
- preview += formatUpgradePrompt("generate_rollback", "Full rollback SQL generation with:\n" +
251
- "- Complete reverse DDL for all migration operations\n" +
252
- "- Flyway schema_history cleanup statements\n" +
253
- "- Irreversibility warnings with manual intervention guidance");
254
- return { content: [{ type: "text", text: preview }] };
255
- }
256
238
  const migration = parseMigration(filename, sql);
257
239
  const report = generateRollback(migration);
258
240
  let output = `## Rollback Script: ${filename}\n\n`;
@@ -31,7 +31,8 @@ export function parseFlywayFilename(filename) {
31
31
  }
32
32
  /**
33
33
  * Split SQL text into individual statements.
34
- * Handles semicolons, ignoring those inside strings and comments.
34
+ * Handles semicolons, ignoring those inside string literals and comments.
35
+ * Single-quoted strings are tracked; escaped quotes ('') are handled correctly.
35
36
  */
36
37
  function splitStatements(sql) {
37
38
  // Remove block comments
@@ -40,16 +41,38 @@ function splitStatements(sql) {
40
41
  cleaned = cleaned.replace(/--.*$/gm, "");
41
42
  const stmts = [];
42
43
  let current = "";
43
- for (const char of cleaned) {
44
- if (char === ";") {
44
+ let inString = false;
45
+ let i = 0;
46
+ while (i < cleaned.length) {
47
+ const char = cleaned[i];
48
+ if (char === "'" && !inString) {
49
+ inString = true;
50
+ current += char;
51
+ i++;
52
+ }
53
+ else if (char === "'" && inString) {
54
+ current += char;
55
+ i++;
56
+ // '' is the SQL escape for a literal single quote inside a string
57
+ if (i < cleaned.length && cleaned[i] === "'") {
58
+ current += cleaned[i];
59
+ i++;
60
+ }
61
+ else {
62
+ inString = false;
63
+ }
64
+ }
65
+ else if (char === ";" && !inString) {
45
66
  const trimmed = current.trim();
46
67
  if (trimmed.length > 0) {
47
68
  stmts.push(trimmed);
48
69
  }
49
70
  current = "";
71
+ i++;
50
72
  }
51
73
  else {
52
74
  current += char;
75
+ i++;
53
76
  }
54
77
  }
55
78
  const last = current.trim();
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "mcp-migration-advisor",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "MCP server for database migration risk analysis — Flyway and Liquibase XML/YAML/SQL support with lock detection and conflict analysis",
5
+ "mcpName": "Migration Advisor",
5
6
  "main": "build/index.js",
6
7
  "bin": {
7
8
  "mcp-migration-advisor": "build/index.js"
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