db-diagram-tool-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.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # db-diagram-tool-mcp
2
+
3
+ A **read-only** [Model Context Protocol](https://modelcontextprotocol.io) server
4
+ (stdio) that lets MCP clients — **Claude Code** and **OpenAI Codex** — read your
5
+ [DB Diagram Tool](https://github.com/kai-exnodes) diagrams: list them, and fetch a
6
+ diagram's schema as JSON, DBML, or SQL DDL.
7
+
8
+ It talks to the DB Diagram Tool backend over its normal REST API, authenticated
9
+ with a **Personal Access Token (PAT)**. It performs **only GET requests** — it can
10
+ never modify your diagrams.
11
+
12
+ ## Tools
13
+
14
+ | Tool | Args | Returns |
15
+ |------|------|---------|
16
+ | `list_diagrams` | — | The diagrams your token can access (id, name, role, visibility, updated time). |
17
+ | `get_diagram` | `id` | Metadata + the full schema snapshot (tables, columns, relationships) as JSON. |
18
+ | `get_diagram_dbml` | `id` | The diagram rendered as [DBML](https://dbml.dbdiagram.io) (dbdiagram.io text format). |
19
+ | `get_diagram_sql` | `id`, `dialect?` (`postgres` \| `mysql`, default `postgres`) | Server-generated `CREATE TABLE` SQL DDL. |
20
+
21
+ ## Requirements
22
+
23
+ - **Node.js ≥ 18** (uses the built-in `fetch`).
24
+ - A **Personal Access Token** for your DB Diagram Tool account (create one in the
25
+ app's Settings → Personal Access Tokens). It looks like `ddt_pat_…`.
26
+ - The backend **origin** URL (e.g. `http://localhost:8090` for local dev, or your
27
+ hosted instance).
28
+
29
+ ## Configuration
30
+
31
+ The server reads two environment variables (set them in your MCP client's server
32
+ config, below):
33
+
34
+ | Variable | Required | Example | Notes |
35
+ |----------|----------|---------|-------|
36
+ | `DDT_BASE_URL` | yes | `http://localhost:8090` | Backend **origin** only — scheme + host[:port], **no** `/api/v1` path. |
37
+ | `DDT_PAT` | yes | `ddt_pat_abc123…` | Your personal access token. Treat it like a password. |
38
+
39
+ ## Build
40
+
41
+ Not published to npm yet — build it locally and point your client at the compiled
42
+ entry (`dist/index.js`):
43
+
44
+ ```bash
45
+ cd db-diagram-tool-mcp
46
+ npm install
47
+ npm run build
48
+ # entry point: <this dir>/dist/index.js (use its ABSOLUTE path below)
49
+ ```
50
+
51
+ ## Install in Claude Code
52
+
53
+ **CLI (adds it for you):**
54
+
55
+ ```bash
56
+ claude mcp add --transport stdio db-diagram-tool \
57
+ --env DDT_BASE_URL=http://localhost:8090 \
58
+ --env DDT_PAT=ddt_pat_your_token_here \
59
+ -- node /ABSOLUTE/PATH/TO/db-diagram-tool-mcp/dist/index.js
60
+ ```
61
+
62
+ `--scope` defaults to `local` (this project, private to you); add `--scope user`
63
+ to enable it everywhere, or `--scope project` to write a shared, git-tracked
64
+ `.mcp.json`.
65
+
66
+ **Or a project `.mcp.json`** (git-tracked; shared with your team):
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "db-diagram-tool": {
72
+ "command": "node",
73
+ "args": ["/ABSOLUTE/PATH/TO/db-diagram-tool-mcp/dist/index.js"],
74
+ "env": {
75
+ "DDT_BASE_URL": "http://localhost:8090",
76
+ "DDT_PAT": "${DDT_PAT}"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ > **Don't commit your token.** Claude Code expands `${VAR}` (and `${VAR:-default}`)
84
+ > in `.mcp.json`, so use `"DDT_PAT": "${DDT_PAT}"` and export `DDT_PAT` in your
85
+ > shell — the raw token stays out of the repo.
86
+
87
+ ## Install in Codex
88
+
89
+ **CLI:**
90
+
91
+ ```bash
92
+ codex mcp add db-diagram-tool \
93
+ --env DDT_BASE_URL=http://localhost:8090 \
94
+ --env DDT_PAT=ddt_pat_your_token_here \
95
+ -- node /ABSOLUTE/PATH/TO/db-diagram-tool-mcp/dist/index.js
96
+ ```
97
+
98
+ (The `--` before the command is required.)
99
+
100
+ **Or `~/.codex/config.toml`** (global) — or `.codex/config.toml` in a project:
101
+
102
+ ```toml
103
+ [mcp_servers.db-diagram-tool]
104
+ command = "node"
105
+ args = ["/ABSOLUTE/PATH/TO/db-diagram-tool-mcp/dist/index.js"]
106
+
107
+ [mcp_servers.db-diagram-tool.env]
108
+ DDT_BASE_URL = "http://localhost:8090"
109
+ DDT_PAT = "ddt_pat_your_token_here"
110
+ ```
111
+
112
+ > Codex TOML does **not** expand `${VAR}` — use literal values, and keep the file
113
+ > private (don't commit a token).
114
+
115
+ ## Try it
116
+
117
+ Once configured, ask your agent things like:
118
+
119
+ - "List my DB Diagram Tool diagrams."
120
+ - "Show the DBML for the diagram named _Blog_."
121
+ - "Give me the Postgres DDL for diagram `<id>`."
122
+
123
+ ## Security
124
+
125
+ - **Read-only.** Only GET endpoints are ever called.
126
+ - Your PAT grants read access to your diagrams — store it like a password, prefer
127
+ `${DDT_PAT}` env expansion (Claude Code) over inlining, and never commit it.
128
+
129
+ ## Notes
130
+
131
+ - `diagramToDbml.ts` is ported verbatim from the web app
132
+ (`db-diagram-tool-fe/src/lib/dbml/diagramToDbml.ts`); keep the two in sync.
133
+ - Built against `@modelcontextprotocol/sdk` v1.30.
package/dist/client.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Read-only REST client for the DB Diagram Tool backend (frozen REST contract;
3
+ * same endpoints the web app uses). All requests are `Authorization: Bearer <pat>`
4
+ * and hit `${baseUrl}/api/v1<path>`. Only GETs — this package never writes.
5
+ */
6
+ const API_PREFIX = "/api/v1";
7
+ /** A structured API error carrying the HTTP status + the backend's error message. */
8
+ export class ApiError extends Error {
9
+ status;
10
+ code;
11
+ constructor(status, message, code) {
12
+ super(message);
13
+ this.status = status;
14
+ this.code = code;
15
+ this.name = "ApiError";
16
+ }
17
+ }
18
+ export class DdtClient {
19
+ config;
20
+ constructor(config) {
21
+ this.config = config;
22
+ }
23
+ async get(path) {
24
+ const url = `${this.config.baseUrl}${API_PREFIX}${path}`;
25
+ let res;
26
+ try {
27
+ res = await fetch(url, {
28
+ headers: {
29
+ Authorization: `Bearer ${this.config.pat}`,
30
+ Accept: "application/json",
31
+ },
32
+ });
33
+ }
34
+ catch (e) {
35
+ throw new ApiError(0, `could not reach the backend at ${this.config.baseUrl} (${e.message}). Check DDT_BASE_URL and that the server is running.`);
36
+ }
37
+ const isJson = res.headers
38
+ .get("content-type")
39
+ ?.includes("application/json");
40
+ const payload = isJson ? await res.json().catch(() => undefined) : undefined;
41
+ if (!res.ok) {
42
+ const env = payload;
43
+ const message = env?.error?.message ?? env?.message ?? res.statusText ?? "request failed";
44
+ const hint = res.status === 401
45
+ ? " (check DDT_PAT — the token may be invalid or expired)"
46
+ : res.status === 403
47
+ ? " (the token lacks access to this diagram)"
48
+ : res.status === 404
49
+ ? " (no such diagram, or it isn't shared with this token)"
50
+ : "";
51
+ throw new ApiError(res.status, `${res.status} ${message}${hint}`, env?.error?.code);
52
+ }
53
+ return payload;
54
+ }
55
+ /** GET /diagrams → the token's accessible diagrams. */
56
+ listDiagrams() {
57
+ return this.get("/diagrams");
58
+ }
59
+ /** GET /diagrams/{id} → metadata. */
60
+ getDiagram(id) {
61
+ return this.get(`/diagrams/${encodeURIComponent(id)}`);
62
+ }
63
+ /** GET /diagrams/{id}/snapshot → the canonical Diagram JSON. */
64
+ getSnapshot(id) {
65
+ return this.get(`/diagrams/${encodeURIComponent(id)}/snapshot`);
66
+ }
67
+ /** GET /diagrams/{id}/export/ddl?dialect=… → server-generated SQL DDL. */
68
+ getDdl(id, dialect) {
69
+ return this.get(`/diagrams/${encodeURIComponent(id)}/export/ddl?dialect=${encodeURIComponent(dialect)}`);
70
+ }
71
+ }
package/dist/config.js ADDED
@@ -0,0 +1,17 @@
1
+ export function loadConfig(env = process.env) {
2
+ const baseUrl = (env.DDT_BASE_URL ?? "").trim().replace(/\/+$/, "");
3
+ const pat = (env.DDT_PAT ?? "").trim();
4
+ const missing = [];
5
+ if (!baseUrl) {
6
+ missing.push("DDT_BASE_URL — the backend origin, e.g. http://localhost:8090");
7
+ }
8
+ if (!pat) {
9
+ missing.push("DDT_PAT — a personal access token (ddt_pat_…)");
10
+ }
11
+ if (missing.length > 0) {
12
+ throw new Error(`db-diagram-tool-mcp: missing required environment variable(s):\n` +
13
+ missing.map((m) => ` - ${m}`).join("\n") +
14
+ `\nSet them in your MCP client's server config (see the README).`);
15
+ }
16
+ return { baseUrl, pat };
17
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Serialize a Diagram to DBML text (dbdiagram.io format). Pure text emit — no
3
+ * dependency. Ported verbatim from db-diagram-tool-fe/src/lib/dbml/diagramToDbml.ts
4
+ * (only the type import path differs); keep in sync with that source of truth.
5
+ *
6
+ * Mapping:
7
+ * - Column: `name type [settings]`, settings from pk/unique/not-null/default/note.
8
+ * - Composite PK (>1 pk column, order = column order): an `indexes { (a,b) [pk] }`
9
+ * block instead of inline `[pk]` (DBML's composite-PK form).
10
+ * - Relationship: `Ref: from.col <op> to.col` where op = `<` (1:N, from=one),
11
+ * `-` (1:1), `<>` (M:N). Our `from` is the one/parent (referenced key) side.
12
+ * - Table note → `Note:`; column note → `[note: …]`; default → `[default: \`…\`]`.
13
+ */
14
+ const SIMPLE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
15
+ /** DBML identifier: bare when simple, else double-quoted. */
16
+ function ident(name) {
17
+ return SIMPLE_IDENT.test(name)
18
+ ? name
19
+ : `"${name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
20
+ }
21
+ /** DBML string literal: single-quoted, or triple-quoted when multi-line. */
22
+ function str(s) {
23
+ if (s.includes("\n"))
24
+ return `'''${s}'''`;
25
+ return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
26
+ }
27
+ /**
28
+ * A column TYPE. DBML accepts `name` / `name(args)` bare; anything else (a space
29
+ * like "double precision", odd chars) is double-quoted so the parser keeps it
30
+ * whole. Empty type (mid-edit) falls back to a placeholder so the DBML stays
31
+ * parseable.
32
+ */
33
+ function typeLit(type) {
34
+ const t = type.trim();
35
+ if (!t)
36
+ return "unknown";
37
+ if (/^[A-Za-z_][A-Za-z0-9_ ]*(\([^)]*\))?$/.test(t) && !t.includes(" ")) {
38
+ // A plain type or type(args); DBML tolerates a single internal space.
39
+ return /\s/.test(t) ? `"${t}"` : t;
40
+ }
41
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
42
+ }
43
+ function columnSettings(col, inComposite) {
44
+ const parts = [];
45
+ // A single PK is inline [pk]; composite PK goes in the indexes block instead.
46
+ if (col.isPrimaryKey && !inComposite)
47
+ parts.push("pk");
48
+ if (col.isUnique)
49
+ parts.push("unique");
50
+ if (!col.isNullable)
51
+ parts.push("not null");
52
+ if (col.default != null && col.default !== "")
53
+ parts.push(`default: \`${col.default}\``);
54
+ if (col.note)
55
+ parts.push(`note: ${str(col.note)}`);
56
+ return parts.length ? ` [${parts.join(", ")}]` : "";
57
+ }
58
+ function tableToDbml(t) {
59
+ const pkCols = t.columns.filter((c) => c.isPrimaryKey);
60
+ const composite = pkCols.length > 1;
61
+ const lines = [`Table ${ident(t.name)} {`];
62
+ for (const c of t.columns) {
63
+ lines.push(` ${ident(c.name)} ${typeLit(c.type)}${columnSettings(c, composite)}`);
64
+ }
65
+ if (composite) {
66
+ lines.push(" indexes {");
67
+ lines.push(` (${pkCols.map((c) => ident(c.name)).join(", ")}) [pk]`);
68
+ lines.push(" }");
69
+ }
70
+ if (t.note)
71
+ lines.push(` Note: ${str(t.note)}`);
72
+ lines.push("}");
73
+ return lines.join("\n");
74
+ }
75
+ const OP = {
76
+ "1:1": "-",
77
+ "1:N": "<",
78
+ "M:N": "<>",
79
+ };
80
+ function refToDbml(r, diagram) {
81
+ const from = diagram.tables.find((t) => t.id === r.fromTableId);
82
+ const to = diagram.tables.find((t) => t.id === r.toTableId);
83
+ const fromCol = from?.columns.find((c) => c.id === r.fromColumnId);
84
+ const toCol = to?.columns.find((c) => c.id === r.toColumnId);
85
+ if (!from || !to || !fromCol || !toCol)
86
+ return null;
87
+ return `Ref: ${ident(from.name)}.${ident(fromCol.name)} ${OP[r.cardinality]} ${ident(to.name)}.${ident(toCol.name)}`;
88
+ }
89
+ export function diagramToDbml(diagram) {
90
+ const blocks = diagram.tables.map(tableToDbml);
91
+ const refs = diagram.relationships
92
+ .map((r) => refToDbml(r, diagram))
93
+ .filter((x) => x !== null);
94
+ const out = [...blocks];
95
+ if (refs.length)
96
+ out.push(refs.join("\n"));
97
+ return out.join("\n\n").trim() + "\n";
98
+ }
package/dist/index.js ADDED
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { loadConfig } from "./config.js";
6
+ import { ApiError, DdtClient } from "./client.js";
7
+ import { diagramToDbml } from "./diagramToDbml.js";
8
+ /**
9
+ * db-diagram-tool-mcp — a read-only stdio MCP server exposing your DB Diagram Tool
10
+ * diagrams to MCP clients (Claude Code, Codex). Four tools: list_diagrams,
11
+ * get_diagram (schema JSON), get_diagram_dbml, get_diagram_sql. Auth is a PAT.
12
+ *
13
+ * stdio protocol note: stdout is the JSON-RPC channel — NEVER write to stdout.
14
+ * All diagnostics go to stderr.
15
+ */
16
+ const SERVER_NAME = "db-diagram-tool";
17
+ const SERVER_VERSION = "0.1.0";
18
+ /** A successful text result. */
19
+ function text(body) {
20
+ return { content: [{ type: "text", text: body }] };
21
+ }
22
+ /** A tool-level error result (isError so the client shows it as a failure, but the
23
+ * server keeps running for the next call). */
24
+ function errorResult(e) {
25
+ const msg = e instanceof ApiError
26
+ ? e.message
27
+ : e instanceof Error
28
+ ? e.message
29
+ : String(e);
30
+ return {
31
+ content: [{ type: "text", text: `Error: ${msg}` }],
32
+ isError: true,
33
+ };
34
+ }
35
+ function buildServer(client) {
36
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
37
+ const readOnly = { readOnlyHint: true, openWorldHint: true };
38
+ server.registerTool("list_diagrams", {
39
+ title: "List diagrams",
40
+ description: "List the DB Diagram Tool diagrams this token can access. Returns id, name, your role, visibility, and last-updated time for each. Use an id with the other tools.",
41
+ annotations: readOnly,
42
+ }, async () => {
43
+ try {
44
+ const { diagrams } = await client.listDiagrams();
45
+ const rows = (diagrams ?? []).map((d) => ({
46
+ id: d.id,
47
+ name: d.name,
48
+ role: d.role,
49
+ visibility: d.visibility,
50
+ updatedAt: d.updatedAt,
51
+ }));
52
+ return text(JSON.stringify(rows, null, 2));
53
+ }
54
+ catch (e) {
55
+ return errorResult(e);
56
+ }
57
+ });
58
+ server.registerTool("get_diagram", {
59
+ title: "Get diagram (schema JSON)",
60
+ description: "Get a diagram's metadata plus its schema (tables, columns with types/keys/nullability, and relationships) as JSON, by id. This is a DENOISED read-view: the editor's table layout coordinates are omitted (use get_diagram_dbml or get_diagram_sql for other views).",
61
+ inputSchema: {
62
+ id: z.string().describe("The diagram id (from list_diagrams)."),
63
+ },
64
+ annotations: readOnly,
65
+ }, async ({ id }) => {
66
+ try {
67
+ const [meta, diagram] = await Promise.all([
68
+ client.getDiagram(id),
69
+ client.getSnapshot(id),
70
+ ]);
71
+ // Denoise for coding agents: drop each table's editor layout coords
72
+ // (position x,y) — pure UI cruft that just adds token noise. Everything
73
+ // schema-relevant stays. (Not the raw contract; see the DBML/SQL tools.)
74
+ const schema = {
75
+ ...diagram,
76
+ tables: diagram.tables.map(({ position: _position, ...table }) => table),
77
+ };
78
+ return text(JSON.stringify({ meta, diagram: schema }, null, 2));
79
+ }
80
+ catch (e) {
81
+ return errorResult(e);
82
+ }
83
+ });
84
+ server.registerTool("get_diagram_dbml", {
85
+ title: "Get diagram as DBML",
86
+ description: "Get a diagram rendered as DBML (the dbdiagram.io text format: Table/Ref blocks), by id. Compact and human-readable.",
87
+ inputSchema: {
88
+ id: z.string().describe("The diagram id (from list_diagrams)."),
89
+ },
90
+ annotations: readOnly,
91
+ }, async ({ id }) => {
92
+ try {
93
+ const diagram = await client.getSnapshot(id);
94
+ return text(diagramToDbml(diagram));
95
+ }
96
+ catch (e) {
97
+ return errorResult(e);
98
+ }
99
+ });
100
+ server.registerTool("get_diagram_sql", {
101
+ title: "Get diagram as SQL DDL",
102
+ description: "Get a diagram's CREATE TABLE SQL DDL for a dialect (postgres or mysql), generated by the backend. Defaults to postgres.",
103
+ inputSchema: {
104
+ id: z.string().describe("The diagram id (from list_diagrams)."),
105
+ dialect: z
106
+ .enum(["postgres", "mysql"])
107
+ .default("postgres")
108
+ .describe("SQL dialect: postgres (default) or mysql."),
109
+ },
110
+ annotations: readOnly,
111
+ }, async ({ id, dialect }) => {
112
+ try {
113
+ const { sql, warnings } = await client.getDdl(id, dialect);
114
+ const header = warnings?.length
115
+ ? warnings.map((w) => `-- warning: ${w}`).join("\n") + "\n"
116
+ : "";
117
+ return text(header + sql);
118
+ }
119
+ catch (e) {
120
+ return errorResult(e);
121
+ }
122
+ });
123
+ return server;
124
+ }
125
+ async function main() {
126
+ const config = loadConfig(); // throws with a clear message if unset
127
+ const client = new DdtClient(config);
128
+ const server = buildServer(client);
129
+ await server.connect(new StdioServerTransport());
130
+ // stderr only — stdout carries the MCP protocol.
131
+ process.stderr.write(`db-diagram-tool-mcp v${SERVER_VERSION} connected (backend: ${config.baseUrl})\n`);
132
+ }
133
+ main().catch((err) => {
134
+ const msg = err instanceof Error ? err.message : String(err);
135
+ process.stderr.write(`${msg}\n`);
136
+ process.exit(1);
137
+ });
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Diagram document types — a copy of the DB Diagram Tool contract-1 models
3
+ * (db-diagram-tool-fe/src/types/diagram.ts, frozen Diagram JSON schema v1.0.0).
4
+ * Kept in sync manually; this package only READS snapshots, so it needs the shape
5
+ * to (a) render DBML and (b) type the API responses.
6
+ */
7
+ export const SCHEMA_VERSION = "1.0";
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "db-diagram-tool-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Read-only Model Context Protocol (stdio) server for DB Diagram Tool — lets Claude Code / Codex read your diagrams (list, schema JSON, DBML, SQL DDL).",
5
+ "type": "module",
6
+ "bin": {
7
+ "db-diagram-tool-mcp": "dist/index.js"
8
+ },
9
+ "files": ["dist", "README.md"],
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json",
15
+ "dev": "tsx src/index.ts",
16
+ "start": "node dist/index.js",
17
+ "typecheck": "tsc -p tsconfig.json --noEmit"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.30.0",
21
+ "zod": "^3.25.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22",
25
+ "tsx": "^4.19.0",
26
+ "typescript": "^5.6.0"
27
+ }
28
+ }