@arvoretech/db-diagram-mcp 1.0.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.
- package/README.md +46 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/mermaid.d.ts +9 -0
- package/dist/mermaid.d.ts.map +1 -0
- package/dist/mermaid.js +183 -0
- package/dist/mermaid.js.map +1 -0
- package/dist/mermaid.test.d.ts +2 -0
- package/dist/mermaid.test.d.ts.map +1 -0
- package/dist/mermaid.test.js +118 -0
- package/dist/mermaid.test.js.map +1 -0
- package/dist/parser.d.ts +4 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +148 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.test.d.ts +2 -0
- package/dist/parser.test.d.ts.map +1 -0
- package/dist/parser.test.js +110 -0
- package/dist/parser.test.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +117 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +37 -0
- package/dist/types.js.map +1 -0
- package/dist/visualizer.d.ts +2 -0
- package/dist/visualizer.d.ts.map +1 -0
- package/dist/visualizer.js +106 -0
- package/dist/visualizer.js.map +1 -0
- package/package.json +43 -0
- package/src/index.ts +23 -0
- package/src/mermaid.test.ts +135 -0
- package/src/mermaid.ts +250 -0
- package/src/parser.test.ts +118 -0
- package/src/parser.ts +185 -0
- package/src/server.ts +163 -0
- package/src/types.ts +88 -0
- package/src/visualizer.ts +116 -0
- package/tsconfig.json +20 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { parseDDL, extractInlineForeignKeys } from "./parser.js";
|
|
4
|
+
import { generateErd, generateDomainMap, explainTable, traceFlow } from "./mermaid.js";
|
|
5
|
+
import { visualize } from "./visualizer.js";
|
|
6
|
+
import {
|
|
7
|
+
GenerateErdParamsSchema,
|
|
8
|
+
GenerateDomainMapParamsSchema,
|
|
9
|
+
ExplainTableParamsSchema,
|
|
10
|
+
TraceFlowParamsSchema,
|
|
11
|
+
VisualizeParamsSchema,
|
|
12
|
+
McpToolResult,
|
|
13
|
+
GenerateErdParams,
|
|
14
|
+
GenerateDomainMapParams,
|
|
15
|
+
ExplainTableParams,
|
|
16
|
+
TraceFlowParams,
|
|
17
|
+
VisualizeParams,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
|
|
20
|
+
export class DbDiagramMCPServer {
|
|
21
|
+
private server: McpServer;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
this.server = new McpServer({
|
|
25
|
+
name: "db-diagram-mcp-server",
|
|
26
|
+
version: "1.0.0",
|
|
27
|
+
});
|
|
28
|
+
this.setupTools();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private setupTools(): void {
|
|
32
|
+
this.server.registerTool(
|
|
33
|
+
"generate_erd",
|
|
34
|
+
{
|
|
35
|
+
title: "Generate ERD",
|
|
36
|
+
description:
|
|
37
|
+
"Generate a Mermaid ER diagram from SQL DDL. Optionally filter to specific tables.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
ddl: GenerateErdParamsSchema.shape.ddl,
|
|
40
|
+
tables: GenerateErdParamsSchema.shape.tables,
|
|
41
|
+
title: GenerateErdParamsSchema.shape.title,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
async (params): Promise<McpToolResult> => {
|
|
45
|
+
const { ddl, tables, title } = params as GenerateErdParams;
|
|
46
|
+
let schema = parseDDL(ddl);
|
|
47
|
+
schema = extractInlineForeignKeys(ddl, schema);
|
|
48
|
+
const diagram = generateErd(schema, { tables, title });
|
|
49
|
+
return { content: [{ type: "text", text: diagram }] };
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
this.server.registerTool(
|
|
54
|
+
"generate_domain_map",
|
|
55
|
+
{
|
|
56
|
+
title: "Generate Domain Map",
|
|
57
|
+
description:
|
|
58
|
+
"Generate a Mermaid diagram showing all tables related to an entry table within N hops.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
ddl: GenerateDomainMapParamsSchema.shape.ddl,
|
|
61
|
+
entryTable: GenerateDomainMapParamsSchema.shape.entryTable,
|
|
62
|
+
depth: GenerateDomainMapParamsSchema.shape.depth,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
async (params): Promise<McpToolResult> => {
|
|
66
|
+
const { ddl, entryTable, depth } = params as GenerateDomainMapParams;
|
|
67
|
+
let schema = parseDDL(ddl);
|
|
68
|
+
schema = extractInlineForeignKeys(ddl, schema);
|
|
69
|
+
const diagram = generateDomainMap(schema, entryTable, depth ?? 3);
|
|
70
|
+
return { content: [{ type: "text", text: diagram }] };
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
this.server.registerTool(
|
|
75
|
+
"explain_table",
|
|
76
|
+
{
|
|
77
|
+
title: "Explain Table",
|
|
78
|
+
description:
|
|
79
|
+
"Show a table's columns, relationships (incoming and outgoing), and a mini ER diagram of its neighborhood.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
ddl: ExplainTableParamsSchema.shape.ddl,
|
|
82
|
+
table: ExplainTableParamsSchema.shape.table,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
async (params): Promise<McpToolResult> => {
|
|
86
|
+
const { ddl, table } = params as ExplainTableParams;
|
|
87
|
+
let schema = parseDDL(ddl);
|
|
88
|
+
schema = extractInlineForeignKeys(ddl, schema);
|
|
89
|
+
const explanation = explainTable(schema, table);
|
|
90
|
+
return { content: [{ type: "text", text: explanation }] };
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
this.server.registerTool(
|
|
95
|
+
"trace_flow",
|
|
96
|
+
{
|
|
97
|
+
title: "Trace Flow",
|
|
98
|
+
description:
|
|
99
|
+
"Find relationship paths between two tables and generate a Mermaid diagram of the connecting tables.",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
ddl: TraceFlowParamsSchema.shape.ddl,
|
|
102
|
+
from: TraceFlowParamsSchema.shape.from,
|
|
103
|
+
to: TraceFlowParamsSchema.shape.to,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
async (params): Promise<McpToolResult> => {
|
|
107
|
+
const { ddl, from, to } = params as TraceFlowParams;
|
|
108
|
+
let schema = parseDDL(ddl);
|
|
109
|
+
schema = extractInlineForeignKeys(ddl, schema);
|
|
110
|
+
const flow = traceFlow(schema, from, to);
|
|
111
|
+
return { content: [{ type: "text", text: flow }] };
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
this.server.registerTool(
|
|
116
|
+
"visualize",
|
|
117
|
+
{
|
|
118
|
+
title: "Visualize Diagram",
|
|
119
|
+
description:
|
|
120
|
+
"Render a Mermaid ER diagram in the browser. Accepts DDL (generates ERD) or raw Mermaid code.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
ddl: VisualizeParamsSchema.shape.ddl,
|
|
123
|
+
mermaid: VisualizeParamsSchema.shape.mermaid,
|
|
124
|
+
tables: VisualizeParamsSchema.shape.tables,
|
|
125
|
+
title: VisualizeParamsSchema.shape.title,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
async (params): Promise<McpToolResult> => {
|
|
129
|
+
const { ddl, mermaid: rawMermaid, tables, title } = params as VisualizeParams;
|
|
130
|
+
|
|
131
|
+
let mermaidCode: string;
|
|
132
|
+
if (rawMermaid) {
|
|
133
|
+
mermaidCode = rawMermaid;
|
|
134
|
+
} else if (ddl) {
|
|
135
|
+
let schema = parseDDL(ddl);
|
|
136
|
+
schema = extractInlineForeignKeys(ddl, schema);
|
|
137
|
+
mermaidCode = generateErd(schema, { tables, title });
|
|
138
|
+
} else {
|
|
139
|
+
return { content: [{ type: "text", text: "Provide either 'ddl' or 'mermaid' parameter." }] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const url = await visualize(mermaidCode);
|
|
143
|
+
return { content: [{ type: "text", text: `Diagram opened at ${url} (auto-closes in 5 minutes)` }] };
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async start(): Promise<void> {
|
|
149
|
+
const transport = new StdioServerTransport();
|
|
150
|
+
await this.server.connect(transport);
|
|
151
|
+
console.error("DB Diagram MCP Server started successfully");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
setupGracefulShutdown(): void {
|
|
155
|
+
const shutdown = async (signal: string): Promise<void> => {
|
|
156
|
+
console.error(`Received ${signal}, shutting down gracefully...`);
|
|
157
|
+
process.exit(0);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
161
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export interface Column {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
nullable: boolean;
|
|
7
|
+
primaryKey: boolean;
|
|
8
|
+
unique: boolean;
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ForeignKey {
|
|
13
|
+
columns: string[];
|
|
14
|
+
referencedTable: string;
|
|
15
|
+
referencedColumns: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Table {
|
|
19
|
+
name: string;
|
|
20
|
+
columns: Column[];
|
|
21
|
+
foreignKeys: ForeignKey[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Schema {
|
|
25
|
+
tables: Table[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const GenerateErdParamsSchema = z.object({
|
|
29
|
+
ddl: z.string().min(1).describe("SQL DDL statements (CREATE TABLE)"),
|
|
30
|
+
tables: z
|
|
31
|
+
.array(z.string())
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Filter to specific tables (omit for all)"),
|
|
34
|
+
title: z.string().optional().describe("Diagram title"),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type GenerateErdParams = z.infer<typeof GenerateErdParamsSchema>;
|
|
38
|
+
|
|
39
|
+
export const GenerateDomainMapParamsSchema = z.object({
|
|
40
|
+
ddl: z.string().min(1).describe("SQL DDL statements (CREATE TABLE)"),
|
|
41
|
+
entryTable: z
|
|
42
|
+
.string()
|
|
43
|
+
.describe("Starting table to trace relationships from"),
|
|
44
|
+
depth: z
|
|
45
|
+
.number()
|
|
46
|
+
.min(1)
|
|
47
|
+
.max(10)
|
|
48
|
+
.default(3)
|
|
49
|
+
.describe("How many relationship hops to follow (default: 3)"),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type GenerateDomainMapParams = z.infer<
|
|
53
|
+
typeof GenerateDomainMapParamsSchema
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
export const ExplainTableParamsSchema = z.object({
|
|
57
|
+
ddl: z.string().min(1).describe("SQL DDL statements (CREATE TABLE)"),
|
|
58
|
+
table: z.string().describe("Table name to explain"),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export type ExplainTableParams = z.infer<typeof ExplainTableParamsSchema>;
|
|
62
|
+
|
|
63
|
+
export const TraceFlowParamsSchema = z.object({
|
|
64
|
+
ddl: z.string().min(1).describe("SQL DDL statements (CREATE TABLE)"),
|
|
65
|
+
from: z.string().describe("Source table"),
|
|
66
|
+
to: z.string().describe("Target table"),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export type TraceFlowParams = z.infer<typeof TraceFlowParamsSchema>;
|
|
70
|
+
|
|
71
|
+
export const VisualizeParamsSchema = z.object({
|
|
72
|
+
ddl: z.string().optional().describe("SQL DDL statements (CREATE TABLE). Provide either ddl or mermaid."),
|
|
73
|
+
mermaid: z.string().optional().describe("Raw Mermaid diagram code to render directly."),
|
|
74
|
+
tables: z.array(z.string()).optional().describe("Filter to specific tables (only when using ddl)"),
|
|
75
|
+
title: z.string().optional().describe("Diagram title"),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export type VisualizeParams = z.infer<typeof VisualizeParamsSchema>;
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
export interface McpToolResult {
|
|
83
|
+
[x: string]: unknown;
|
|
84
|
+
content: Array<{
|
|
85
|
+
type: "text";
|
|
86
|
+
text: string;
|
|
87
|
+
}>;
|
|
88
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createServer, Server } from "node:http";
|
|
2
|
+
import { exec } from "node:child_process";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
|
|
5
|
+
export function visualize(mermaidCode: string): Promise<string> {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const html = buildHtml(mermaidCode);
|
|
8
|
+
const server: Server = createServer((req, res) => {
|
|
9
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
10
|
+
res.end(html);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
server.listen(0, "127.0.0.1", () => {
|
|
14
|
+
const address = server.address();
|
|
15
|
+
if (!address || typeof address === "string") {
|
|
16
|
+
reject(new Error("Failed to get server address"));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const url = `http://127.0.0.1:${address.port}`;
|
|
21
|
+
openBrowser(url);
|
|
22
|
+
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
server.close();
|
|
25
|
+
}, 300_000);
|
|
26
|
+
|
|
27
|
+
resolve(url);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
server.on("error", reject);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function openBrowser(url: string): void {
|
|
35
|
+
const cmd =
|
|
36
|
+
platform() === "darwin"
|
|
37
|
+
? `open "${url}"`
|
|
38
|
+
: platform() === "win32"
|
|
39
|
+
? `start "${url}"`
|
|
40
|
+
: `xdg-open "${url}"`;
|
|
41
|
+
|
|
42
|
+
exec(cmd, () => {});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildHtml(mermaidCode: string): string {
|
|
46
|
+
const escaped = mermaidCode.replace(/</g, "<").replace(/>/g, ">");
|
|
47
|
+
|
|
48
|
+
return `<!DOCTYPE html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="UTF-8">
|
|
52
|
+
<title>DB Diagram</title>
|
|
53
|
+
<style>
|
|
54
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
55
|
+
html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a2e; }
|
|
56
|
+
#viewport { width: 100%; height: 100%; cursor: grab; overflow: hidden; }
|
|
57
|
+
#viewport.dragging { cursor: grabbing; }
|
|
58
|
+
#diagram { display: inline-block; padding: 2rem; transform-origin: 0 0; }
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body>
|
|
62
|
+
<div id="viewport">
|
|
63
|
+
<div id="diagram">
|
|
64
|
+
<pre class="mermaid">${escaped}</pre>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<script type="module">
|
|
68
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
69
|
+
mermaid.initialize({ startOnLoad: true, theme: 'dark', er: { useMaxWidth: false, layoutDirection: 'LR' } });
|
|
70
|
+
|
|
71
|
+
const viewport = document.getElementById('viewport');
|
|
72
|
+
const diagram = document.getElementById('diagram');
|
|
73
|
+
let isPanning = false;
|
|
74
|
+
let startX = 0, startY = 0;
|
|
75
|
+
let offsetX = 0, offsetY = 0;
|
|
76
|
+
let scale = 1;
|
|
77
|
+
|
|
78
|
+
function applyTransform() {
|
|
79
|
+
diagram.style.transform = \`translate(\${offsetX}px, \${offsetY}px) scale(\${scale})\`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
viewport.addEventListener('mousedown', (e) => {
|
|
83
|
+
isPanning = true;
|
|
84
|
+
startX = e.clientX - offsetX;
|
|
85
|
+
startY = e.clientY - offsetY;
|
|
86
|
+
viewport.classList.add('dragging');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
window.addEventListener('mousemove', (e) => {
|
|
90
|
+
if (!isPanning) return;
|
|
91
|
+
offsetX = e.clientX - startX;
|
|
92
|
+
offsetY = e.clientY - startY;
|
|
93
|
+
applyTransform();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
window.addEventListener('mouseup', () => {
|
|
97
|
+
isPanning = false;
|
|
98
|
+
viewport.classList.remove('dragging');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
viewport.addEventListener('wheel', (e) => {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
|
104
|
+
const newScale = Math.min(Math.max(scale * delta, 0.1), 5);
|
|
105
|
+
const rect = viewport.getBoundingClientRect();
|
|
106
|
+
const mx = e.clientX - rect.left;
|
|
107
|
+
const my = e.clientY - rect.top;
|
|
108
|
+
offsetX -= (mx - offsetX) * (newScale / scale - 1);
|
|
109
|
+
offsetY -= (my - offsetY) * (newScale / scale - 1);
|
|
110
|
+
scale = newScale;
|
|
111
|
+
applyTransform();
|
|
112
|
+
}, { passive: false });
|
|
113
|
+
</script>
|
|
114
|
+
</body>
|
|
115
|
+
</html>`;
|
|
116
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"outDir": "./dist",
|
|
16
|
+
"rootDir": "./src"
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "coverage"]
|
|
20
|
+
}
|