locksmith-mcp 0.1.0

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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/dist/analyzer/astUtils.d.ts +19 -0
  4. package/dist/analyzer/astUtils.js +50 -0
  5. package/dist/analyzer/astUtils.js.map +1 -0
  6. package/dist/analyzer/engine.d.ts +11 -0
  7. package/dist/analyzer/engine.js +87 -0
  8. package/dist/analyzer/engine.js.map +1 -0
  9. package/dist/analyzer/parse.d.ts +7 -0
  10. package/dist/analyzer/parse.js +165 -0
  11. package/dist/analyzer/parse.js.map +1 -0
  12. package/dist/analyzer/rules/addCheckConstraintNoNotValid.d.ts +2 -0
  13. package/dist/analyzer/rules/addCheckConstraintNoNotValid.js +24 -0
  14. package/dist/analyzer/rules/addCheckConstraintNoNotValid.js.map +1 -0
  15. package/dist/analyzer/rules/addColumnNotNullNoDefault.d.ts +2 -0
  16. package/dist/analyzer/rules/addColumnNotNullNoDefault.js +42 -0
  17. package/dist/analyzer/rules/addColumnNotNullNoDefault.js.map +1 -0
  18. package/dist/analyzer/rules/addColumnVolatileDefault.d.ts +2 -0
  19. package/dist/analyzer/rules/addColumnVolatileDefault.js +36 -0
  20. package/dist/analyzer/rules/addColumnVolatileDefault.js.map +1 -0
  21. package/dist/analyzer/rules/addForeignKeyValidating.d.ts +2 -0
  22. package/dist/analyzer/rules/addForeignKeyValidating.js +26 -0
  23. package/dist/analyzer/rules/addForeignKeyValidating.js.map +1 -0
  24. package/dist/analyzer/rules/alterColumnType.d.ts +2 -0
  25. package/dist/analyzer/rules/alterColumnType.js +31 -0
  26. package/dist/analyzer/rules/alterColumnType.js.map +1 -0
  27. package/dist/analyzer/rules/createIndexNonConcurrent.d.ts +2 -0
  28. package/dist/analyzer/rules/createIndexNonConcurrent.js +24 -0
  29. package/dist/analyzer/rules/createIndexNonConcurrent.js.map +1 -0
  30. package/dist/analyzer/rules/dropColumnOrTable.d.ts +2 -0
  31. package/dist/analyzer/rules/dropColumnOrTable.js +31 -0
  32. package/dist/analyzer/rules/dropColumnOrTable.js.map +1 -0
  33. package/dist/analyzer/rules/index.d.ts +7 -0
  34. package/dist/analyzer/rules/index.js +28 -0
  35. package/dist/analyzer/rules/index.js.map +1 -0
  36. package/dist/analyzer/rules/indexConcurrentlyInTransaction.d.ts +2 -0
  37. package/dist/analyzer/rules/indexConcurrentlyInTransaction.js +23 -0
  38. package/dist/analyzer/rules/indexConcurrentlyInTransaction.js.map +1 -0
  39. package/dist/analyzer/rules/renameColumnOrTable.d.ts +2 -0
  40. package/dist/analyzer/rules/renameColumnOrTable.js +29 -0
  41. package/dist/analyzer/rules/renameColumnOrTable.js.map +1 -0
  42. package/dist/analyzer/rules/setNotNull.d.ts +2 -0
  43. package/dist/analyzer/rules/setNotNull.js +30 -0
  44. package/dist/analyzer/rules/setNotNull.js.map +1 -0
  45. package/dist/analyzer/suppress.d.ts +23 -0
  46. package/dist/analyzer/suppress.js +36 -0
  47. package/dist/analyzer/suppress.js.map +1 -0
  48. package/dist/analyzer/types.d.ts +93 -0
  49. package/dist/analyzer/types.js +2 -0
  50. package/dist/analyzer/types.js.map +1 -0
  51. package/dist/analyzer/verdict.d.ts +9 -0
  52. package/dist/analyzer/verdict.js +35 -0
  53. package/dist/analyzer/verdict.js.map +1 -0
  54. package/dist/data/lockMatrix.d.ts +17 -0
  55. package/dist/data/lockMatrix.js +65 -0
  56. package/dist/data/lockMatrix.js.map +1 -0
  57. package/dist/format.d.ts +3 -0
  58. package/dist/format.js +52 -0
  59. package/dist/format.js.map +1 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/server.d.ts +3 -0
  64. package/dist/server.js +128 -0
  65. package/dist/server.js.map +1 -0
  66. package/package.json +60 -0
@@ -0,0 +1,65 @@
1
+ export const LOCK_MODES = [
2
+ {
3
+ mode: "ACCESS SHARE",
4
+ blocks: "Only conflicts with ACCESS EXCLUSIVE. Taken by plain SELECT.",
5
+ acquiredBy: ["SELECT"],
6
+ },
7
+ {
8
+ mode: "ROW SHARE",
9
+ blocks: "Conflicts with EXCLUSIVE and ACCESS EXCLUSIVE.",
10
+ acquiredBy: ["SELECT ... FOR UPDATE / FOR SHARE"],
11
+ },
12
+ {
13
+ mode: "ROW EXCLUSIVE",
14
+ blocks: "Conflicts with SHARE and above. Normal writes take this.",
15
+ acquiredBy: ["INSERT", "UPDATE", "DELETE"],
16
+ },
17
+ {
18
+ mode: "SHARE UPDATE EXCLUSIVE",
19
+ blocks: "Blocks schema changes and VACUUM, but NOT reads or writes.",
20
+ acquiredBy: ["CREATE INDEX CONCURRENTLY", "VALIDATE CONSTRAINT", "ALTER TABLE ... ADD ... NOT VALID"],
21
+ },
22
+ {
23
+ mode: "SHARE",
24
+ blocks: "Blocks all writes (INSERT/UPDATE/DELETE); reads still allowed.",
25
+ acquiredBy: ["CREATE INDEX (non-concurrent)"],
26
+ },
27
+ {
28
+ mode: "SHARE ROW EXCLUSIVE",
29
+ blocks: "Blocks writes and other schema changes on the table(s).",
30
+ acquiredBy: ["ALTER TABLE ... ADD FOREIGN KEY (validating, both tables)"],
31
+ },
32
+ {
33
+ mode: "EXCLUSIVE",
34
+ blocks: "Blocks reads-for-update and all writes; plain SELECT still allowed.",
35
+ acquiredBy: ["REFRESH MATERIALIZED VIEW CONCURRENTLY"],
36
+ },
37
+ {
38
+ mode: "ACCESS EXCLUSIVE",
39
+ blocks: "Blocks EVERYTHING, including plain SELECT. The most disruptive lock.",
40
+ acquiredBy: [
41
+ "ALTER TABLE ... ALTER COLUMN ... TYPE",
42
+ "ALTER TABLE ... SET NOT NULL",
43
+ "ALTER TABLE ... ADD CHECK (validating)",
44
+ "DROP TABLE / DROP COLUMN",
45
+ "TRUNCATE",
46
+ ],
47
+ },
48
+ ];
49
+ export function renderLockMatrix() {
50
+ const lines = [
51
+ "# PostgreSQL Table-Level Locks",
52
+ "",
53
+ "What each lock blocks, and the operations that take it. ACCESS EXCLUSIVE is",
54
+ "the one to fear: it blocks even plain SELECTs.",
55
+ "",
56
+ ];
57
+ for (const m of LOCK_MODES) {
58
+ lines.push(`## ${m.mode}`);
59
+ lines.push(`- Blocks: ${m.blocks}`);
60
+ lines.push(`- Acquired by: ${m.acquiredBy.join("; ")}`);
61
+ lines.push("");
62
+ }
63
+ return lines.join("\n");
64
+ }
65
+ //# sourceMappingURL=lockMatrix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lockMatrix.js","sourceRoot":"","sources":["../../src/data/lockMatrix.ts"],"names":[],"mappings":"AAgBA,MAAM,CAAC,MAAM,UAAU,GAAe;IACpC;QACE,IAAI,EAAE,cAAc;QACpB,MAAM,EAAE,8DAA8D;QACtE,UAAU,EAAE,CAAC,QAAQ,CAAC;KACvB;IACD;QACE,IAAI,EAAE,WAAW;QACjB,MAAM,EAAE,gDAAgD;QACxD,UAAU,EAAE,CAAC,mCAAmC,CAAC;KAClD;IACD;QACE,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE,0DAA0D;QAClE,UAAU,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC;KAC3C;IACD;QACE,IAAI,EAAE,wBAAwB;QAC9B,MAAM,EAAE,4DAA4D;QACpE,UAAU,EAAE,CAAC,2BAA2B,EAAE,qBAAqB,EAAE,mCAAmC,CAAC;KACtG;IACD;QACE,IAAI,EAAE,OAAO;QACb,MAAM,EAAE,gEAAgE;QACxE,UAAU,EAAE,CAAC,+BAA+B,CAAC;KAC9C;IACD;QACE,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,yDAAyD;QACjE,UAAU,EAAE,CAAC,2DAA2D,CAAC;KAC1E;IACD;QACE,IAAI,EAAE,WAAW;QACjB,MAAM,EAAE,qEAAqE;QAC7E,UAAU,EAAE,CAAC,wCAAwC,CAAC;KACvD;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,MAAM,EAAE,sEAAsE;QAC9E,UAAU,EAAE;YACV,uCAAuC;YACvC,8BAA8B;YAC9B,wCAAwC;YACxC,0BAA0B;YAC1B,UAAU;SACX;KACF;CACF,CAAC;AAEF,MAAM,UAAU,gBAAgB;IAC9B,MAAM,KAAK,GAAG;QACZ,gCAAgC;QAChC,EAAE;QACF,6EAA6E;QAC7E,gDAAgD;QAChD,EAAE;KACH,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { AnalysisResult } from "./analyzer/types.js";
2
+ /** Render an analysis result as Markdown for human (and model) consumption. */
3
+ export declare function renderReport(result: AnalysisResult): string;
package/dist/format.js ADDED
@@ -0,0 +1,52 @@
1
+ const ICON = { critical: "🛑", warning: "⚠️", info: "ℹ️" };
2
+ const VERDICT_LINE = {
3
+ BLOCK: "🛑 BLOCK — do not ship as written",
4
+ REVIEW: "⚠️ REVIEW — confirm this is safe for your tables",
5
+ PASS: "✅ PASS — no locking hazards detected",
6
+ };
7
+ /** Render an analysis result as Markdown for human (and model) consumption. */
8
+ export function renderReport(result) {
9
+ const lines = [];
10
+ lines.push(`# Locksmith — Migration Safety Report`);
11
+ lines.push("");
12
+ lines.push(`**Verdict: ${VERDICT_LINE[result.verdict]}**`);
13
+ lines.push("");
14
+ lines.push(result.summary);
15
+ lines.push("");
16
+ lines.push(`_Analyzed ${result.stats.statements} statement(s); ` +
17
+ `${result.stats.critical} critical, ${result.stats.warning} warning, ` +
18
+ `${result.stats.suppressed} suppressed, ${result.stats.unparsed} unparsed._`);
19
+ if (result.findings.length === 0) {
20
+ return lines.join("\n");
21
+ }
22
+ lines.push("");
23
+ lines.push("---");
24
+ for (const f of result.findings) {
25
+ lines.push("");
26
+ lines.push(renderFinding(f));
27
+ }
28
+ return lines.join("\n");
29
+ }
30
+ function renderFinding(f) {
31
+ const out = [];
32
+ out.push(`## ${ICON[f.severity]} ${f.title} \`${f.ruleId}\` (line ${f.line})`);
33
+ out.push("");
34
+ out.push(`> \`${f.statement}\``);
35
+ out.push("");
36
+ out.push(`**Problem:** ${f.message}`);
37
+ if (f.lockTaken)
38
+ out.push(`**Lock taken:** ${f.lockTaken}`);
39
+ out.push(`**Why it matters:** ${f.rationale}`);
40
+ out.push(`**Fix:** ${f.remediation}`);
41
+ if (f.suggestedRewrite) {
42
+ out.push("");
43
+ out.push("**Suggested rewrite:**");
44
+ out.push("```sql");
45
+ out.push(f.suggestedRewrite);
46
+ out.push("```");
47
+ }
48
+ if (f.docsUrl)
49
+ out.push(`📖 ${f.docsUrl}`);
50
+ return out.join("\n");
51
+ }
52
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createServer } from "./server.js";
4
+ /**
5
+ * Entry point: run Locksmith as a stdio MCP server. An MCP client (Claude
6
+ * Desktop/Code, the MCP Inspector, etc.) launches this process and speaks
7
+ * JSON-RPC over stdin/stdout — so nothing may be written to stdout except the
8
+ * protocol. All diagnostics go to stderr.
9
+ */
10
+ async function main() {
11
+ const server = createServer();
12
+ const transport = new StdioServerTransport();
13
+ await server.connect(transport);
14
+ process.stderr.write("locksmith MCP server running on stdio\n");
15
+ }
16
+ main().catch((err) => {
17
+ process.stderr.write(`locksmith failed to start: ${String(err)}\n`);
18
+ process.exit(1);
19
+ });
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;GAKG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;AAClE,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /** Build and configure the Locksmith MCP server (transport-agnostic). */
3
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,128 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { analyzeMigration } from "./analyzer/engine.js";
4
+ import { rules } from "./analyzer/rules/index.js";
5
+ import { LOCK_MODES, renderLockMatrix } from "./data/lockMatrix.js";
6
+ import { renderReport } from "./format.js";
7
+ const findingSchema = z.object({
8
+ ruleId: z.string(),
9
+ title: z.string(),
10
+ severity: z.enum(["info", "warning", "critical"]),
11
+ line: z.number(),
12
+ statement: z.string(),
13
+ lockTaken: z.string().optional(),
14
+ message: z.string(),
15
+ rationale: z.string(),
16
+ remediation: z.string(),
17
+ suggestedRewrite: z.string().optional(),
18
+ docsUrl: z.string().optional(),
19
+ });
20
+ const analysisOutputSchema = {
21
+ verdict: z.enum(["PASS", "REVIEW", "BLOCK"]),
22
+ summary: z.string(),
23
+ stats: z.object({
24
+ statements: z.number(),
25
+ findings: z.number(),
26
+ critical: z.number(),
27
+ warning: z.number(),
28
+ info: z.number(),
29
+ suppressed: z.number(),
30
+ unparsed: z.number(),
31
+ }),
32
+ findings: z.array(findingSchema),
33
+ };
34
+ /** Build and configure the Locksmith MCP server (transport-agnostic). */
35
+ export function createServer() {
36
+ const server = new McpServer({ name: "locksmith", version: "0.1.0" });
37
+ // --- Tool: analyze_migration -------------------------------------------
38
+ server.registerTool("analyze_migration", {
39
+ title: "Analyze a SQL migration for locking hazards",
40
+ description: "Lint a PostgreSQL migration for operations that take dangerous locks (table rewrites, " +
41
+ "blocking index builds, validating constraints, destructive/breaking changes) and return " +
42
+ "a PASS/REVIEW/BLOCK verdict with per-statement findings and safe rewrites. " +
43
+ "Call this before applying or approving any migration.",
44
+ inputSchema: {
45
+ sql: z.string().describe("The full SQL migration script to analyze."),
46
+ assumeLargeTables: z
47
+ .boolean()
48
+ .optional()
49
+ .describe("Assume target tables are large/hot, so scans and rewrites are flagged. Default true (fail safe)."),
50
+ },
51
+ outputSchema: analysisOutputSchema,
52
+ }, async ({ sql, assumeLargeTables }) => {
53
+ const result = analyzeMigration(sql, { assumeLargeTables });
54
+ return {
55
+ content: [{ type: "text", text: renderReport(result) }],
56
+ structuredContent: result,
57
+ };
58
+ });
59
+ // --- Tool: explain_lock ------------------------------------------------
60
+ server.registerTool("explain_lock", {
61
+ title: "Explain a PostgreSQL lock mode",
62
+ description: "Given a lock mode name (e.g. 'ACCESS EXCLUSIVE') or a fragment of an operation, explain " +
63
+ "what it blocks and which operations acquire it.",
64
+ inputSchema: {
65
+ query: z.string().describe("A lock mode name or operation fragment, e.g. 'ACCESS EXCLUSIVE' or 'create index'."),
66
+ },
67
+ }, async ({ query }) => {
68
+ const q = query.toLowerCase();
69
+ const matches = LOCK_MODES.filter((m) => m.mode.toLowerCase().includes(q) ||
70
+ m.acquiredBy.some((op) => op.toLowerCase().includes(q) || q.includes(op.toLowerCase())));
71
+ const chosen = matches.length > 0 ? matches : LOCK_MODES;
72
+ const text = chosen
73
+ .map((m) => `### ${m.mode}\n- Blocks: ${m.blocks}\n- Acquired by: ${m.acquiredBy.join("; ")}`)
74
+ .join("\n\n");
75
+ return { content: [{ type: "text", text }] };
76
+ });
77
+ // --- Resource: the Postgres lock matrix --------------------------------
78
+ server.registerResource("lock-matrix", "locksmith://lock-matrix", {
79
+ title: "PostgreSQL lock compatibility matrix",
80
+ description: "Which lock each operation takes and what that lock blocks.",
81
+ mimeType: "text/markdown",
82
+ }, async (uri) => ({
83
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text: renderLockMatrix() }],
84
+ }));
85
+ // --- Resource: the rule catalog ----------------------------------------
86
+ server.registerResource("rules", "locksmith://rules", {
87
+ title: "Locksmith rule catalog",
88
+ description: "All migration-safety rules, their severity, and rationale.",
89
+ mimeType: "application/json",
90
+ }, async (uri) => ({
91
+ contents: [
92
+ {
93
+ uri: uri.href,
94
+ mimeType: "application/json",
95
+ text: JSON.stringify(rules.map((r) => ({
96
+ id: r.id,
97
+ title: r.title,
98
+ severity: r.severity,
99
+ rationale: r.rationale,
100
+ remediation: r.remediation,
101
+ docsUrl: r.docsUrl,
102
+ })), null, 2),
103
+ },
104
+ ],
105
+ }));
106
+ // --- Prompt: review-migration ------------------------------------------
107
+ server.registerPrompt("review-migration", {
108
+ title: "Review a migration for safety",
109
+ description: "Analyze a migration with Locksmith and summarize the risk for a PR comment.",
110
+ argsSchema: { sql: z.string().describe("The migration SQL to review.") },
111
+ }, ({ sql }) => ({
112
+ messages: [
113
+ {
114
+ role: "user",
115
+ content: {
116
+ type: "text",
117
+ text: "Call the `analyze_migration` tool on the SQL below. Then write a concise PR review " +
118
+ "comment: state the verdict, list each blocking/risky operation with its safe rewrite, " +
119
+ "and end with a clear ship / don't-ship recommendation.\n\n```sql\n" +
120
+ sql +
121
+ "\n```",
122
+ },
123
+ },
124
+ ],
125
+ }));
126
+ return server;
127
+ }
128
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "locksmith-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Locksmith — an MCP server that lints SQL migrations for dangerous locking operations and suggests safe rewrites (PostgreSQL).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Christopher King <chris@cking.me>",
8
+ "homepage": "https://github.com/cxk280/locksmith#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/cxk280/locksmith.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/cxk280/locksmith/issues"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "postgres",
20
+ "postgresql",
21
+ "migration",
22
+ "database",
23
+ "lint",
24
+ "safety",
25
+ "ddl",
26
+ "locks"
27
+ ],
28
+ "bin": {
29
+ "locksmith": "dist/index.js",
30
+ "locksmith-mcp": "dist/index.js"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "dev": "tsx src/index.ts",
38
+ "start": "node dist/index.js",
39
+ "test": "vitest run",
40
+ "inspect": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js",
41
+ "prepublishOnly": "npm run build && npm test"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.12.0",
48
+ "pgsql-ast-parser": "^12.0.1",
49
+ "zod": "^3.23.8"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.0.0",
53
+ "tsx": "^4.19.0",
54
+ "typescript": "^5.6.0",
55
+ "vitest": "^2.1.0"
56
+ },
57
+ "engines": {
58
+ "node": ">=18"
59
+ }
60
+ }