Skip to main content
Glama
README.md
# MCP draw.io server

`mcp-drawio-server` is a local MCP server that turns a relational schema into
an editable entity-relationship diagram in draw.io. It can reflect a live
database, parse SQL DDL, read a YAML or JSON schema specification, or reopen an
existing uncompressed `.drawio` file. It also exposes granular tools for
changing tables, columns, relations, and layout before saving the diagram.

The schema model is the source of truth. Every save regenerates the complete
mxGraph document instead of patching XML cells in place.

Relations use deterministic, obstacle-aware orthogonal routing. Each endpoint
gets a private port, crowded hubs fan out into separate lanes, recursive
relations loop outside their table, and persistent waypoints keep the route
stable when the file is reopened. Adjacent hub ports keep a visible pitch,
terminal stubs cannot backtrack over themselves, and arc jumps make the
remaining crossings explicit. Crow's-foot markers include nullable-FK
optionality (`0..1`, `0..N`, `1`, and `N`).

## Requirements and installation

- Python 3.14 or newer
- [`uv`](https://docs.astral.sh/uv/)
- draw.io Desktop is optional for generation, but required for interactive
  viewing and the final compatibility check

Install the locked environment:

```bash
git clone https://github.com/marcelovillanuevam-code/MCP-Draw.IO.git
cd MCP-Draw.IO
uv sync --locked
```

Run the stdio server directly while developing:

```bash
uv run --locked --no-sync mcp-drawio-server
```

The server communicates over standard input and output, so it normally appears
to wait silently when started by hand.

## Register with Claude Code

Register it as a user-scoped utility so it is available in every project. Use
absolute paths because Claude starts the command directly rather than through
an interactive shell.

```bash
claude mcp add --transport stdio --scope user drawio -- \
  /absolute/path/to/uv run \
  --project /absolute/path/to/MCP-Draw.IO \
  --locked --no-sync \
  mcp-drawio-server
```

`uv run --project` selects this project's environment without changing the
server process's working directory. This matters because relative output paths
normally resolve from the Claude project that launched the server. Do not
replace it with `uv --directory`, which changes the working directory to this
repository.

Confirm the registration, then start a new Claude Code session:

```bash
claude mcp get drawio
claude mcp list
```

Use `--scope local` instead if the server should be available only in the
current Claude project.

## Configuration and output paths

The only server setting is optional:

```text
MCP_DRAWIO_OUTPUT_DIR=/absolute/path/to/diagrams
```

It is the base directory for relative `.drawio` save and open paths. If it is
unset, the process working directory is used. Absolute paths always take
precedence, and a missing `.drawio` extension is added automatically. The
server creates missing parent directories when it saves.

The project does not load `.env` files itself; `.env.example` documents the
variable for shells, process managers, or MCP client configuration. To pin a
single output directory in Claude Code, add the environment setting when
registering the server:

```bash
claude mcp add --transport stdio \
  -e MCP_DRAWIO_OUTPUT_DIR=/absolute/path/to/diagrams \
  --scope user drawio -- \
  /absolute/path/to/uv run \
  --project /absolute/path/to/MCP-Draw.IO \
  --locked --no-sync \
  mcp-drawio-server
```

Named diagrams live only in the server process. Save important work before the
session ends, and use `open_diagram` to restore it in a later session.

## Input formats

### Live database

`load_database_schema` uses SQLAlchemy reflection. It supports a named schema
and include/exclude table filters. SQLite works without an extra package; other
engines require a DBAPI driver.

| Database | Example URL | Driver command |
| --- | --- | --- |
| SQLite | `sqlite:////absolute/path/store.db` | Built in |
| PostgreSQL | `postgresql+psycopg://user:password@host/database` | `uv add "psycopg[binary]"` |
| MySQL/MariaDB | `mysql+pymysql://user:password@host/database` | `uv add pymysql` |

Other SQLAlchemy dialects may work after their driver is installed, but are not
part of the base environment. Run driver installation commands from the
project root so `pyproject.toml` and `uv.lock` stay in sync.

### SQL DDL

`load_ddl_schema` accepts SQL text or a file path. Pass a sqlglot dialect such
as `postgres`, `mysql`, `sqlite`, or `tsql` when the syntax is dialect-specific.
Only `CREATE TABLE` statements contribute to the diagram; unrelated statements
are skipped. Foreign keys to tables outside a partial DDL input are omitted.
Constraints must appear inside `CREATE TABLE`; dump-style `ALTER TABLE ... ADD
CONSTRAINT` statements and standalone `CREATE UNIQUE INDEX` statements are not
currently imported by the DDL parser.

### YAML or JSON specification

`load_spec_schema` accepts text or a file path. JSON is parsed as a subset of
YAML. Both expanded objects and concise column/relation forms are supported:

```yaml
name: shop
tables:
  customer:
    columns:
      - "customer_id: INTEGER pk"
      - "email: VARCHAR(255) not null unique"
  orders:
    columns:
      - "order_id: INTEGER pk"
      - "customer_id: INTEGER required"
relations:
  - orders.customer_id -> customer.customer_id
```

Column shorthand recognizes `pk`/`primary key`, `unique`/`uq`, `not null`/
`not_null`/`notnull`/`required`, and `null`/`nullable`. A `<table>_id` column
may infer a relation to a matching table with a single-column primary key when
no explicit relation exists.

Composite unique keys use the expanded table form and participate in
cardinality inference:

```yaml
tables:
  enrollment:
    columns:
      - "student_id: INTEGER"
      - "course_id: INTEGER"
    unique_keys:
      - [student_id, course_id]
```

### draw.io XML

`open_diagram` reads uncompressed `.drawio` or `.xml` mxGraph documents.
Generated files use draw.io's native table, table-row, and ERD edge shapes.

## MCP tools

The server exposes 16 tools. A loader creates a named diagram or replaces the
schema of an existing diagram with that name.

| Tool | Purpose |
| --- | --- |
| `load_database_schema(url, diagram, schema=None, include_tables=None, exclude_tables=None)` | Reflect selected tables from a live database. |
| `load_ddl_schema(diagram, ddl=None, path=None, dialect=None)` | Load `CREATE TABLE` statements from inline DDL or one file. |
| `load_spec_schema(diagram, spec=None, path=None)` | Load an inline YAML/JSON specification or one file. |
| `open_diagram(path, diagram=None)` | Open an existing uncompressed draw.io document. |
| `list_diagrams()` | List the diagrams currently held in memory. |
| `describe_diagram(diagram)` | Summarize tables, primary keys, and relations. |
| `add_table(diagram, table, columns)` | Add a table; `columns` is a list of column shorthand strings. |
| `remove_table(diagram, table)` | Remove a table and relations that touch it. |
| `add_column(diagram, table, column)` | Add one column from shorthand. |
| `remove_column(diagram, table, column)` | Remove a column and invalidated relations. |
| `add_relation(diagram, source, target, cardinality=None, name=None)` | Add a relation between endpoints such as `orders.customer_id` and `customer.customer_id`. |
| `remove_relation(diagram, source, target)` | Remove the relation matching the two endpoints. |
| `move_table(diagram, table, x, y)` | Set a table's draw.io coordinates. |
| `relayout_diagram(diagram)` | Recompute the complete automatic layout. |
| `save_diagram(diagram, path=None)` | Regenerate and save `.drawio` XML; omit `path` after the first save. |
| `export_spec(diagram, format="yaml")` | Return the current schema as YAML or JSON. |

Composite endpoints use parentheses, for example
`order_line.(order_id,line_no)`. Explicit cardinalities are `one-to-one`,
`one-to-many`, and `many-to-many`; when omitted, the server infers cardinality
from keys where possible.

The MCP server produces `.drawio`/`.xml` and YAML/JSON text. It does not export
PNG, SVG, or PDF itself; use draw.io Desktop or its CLI for those formats.

## Security

- Treat a database URL as a secret. Do not commit it, paste it into issue
  reports, or store it in `.env.example`; MCP tool calls and client logs may
  retain arguments.
- Percent-encode reserved characters in usernames and passwords, and quote a
  URL when passing it through a shell. Prefer short-lived credentials so an
  accidentally retained URL has limited value.
- Use a dedicated, least-privilege, read-only database account and encrypted
  transport for remote databases. Reflection reads metadata, but the database
  still receives a real connection from this process.
- Schema names, table names, column names, and comments returned by reflection
  become available to the MCP client and model. Do not introspect sensitive
  production metadata unless that disclosure is acceptable.
- File tools can read or write any path permitted to the server process. There
  is no built-in path allowlist, so use a restricted OS account or container
  when processing untrusted requests.
- DDL and YAML/JSON inputs are parsed locally and are not executed against a
  database.

## Round-trip limitations

- Compressed draw.io documents cannot be opened. In draw.io Desktop, use
  **File > Properties**, clear **Compressed**, and save again.
- Multi-page files are rejected rather than silently dropping pages. Save the
  ERD page as a separate uncompressed file before calling `open_diagram`.
- Generated files contain a hidden `mcp-schema-metadata` JSON cell. The server
  prefers its canonical schema on reopen, while current visible table geometry
  wins over stale coordinates in the hidden payload.
- Manual edits to visible table shapes do not update the hidden metadata.
  Moves and resizes are recovered from visible geometry, but manually renamed
  columns, added rows, or new relations can be ignored the next time the MCP
  server opens the file. Make structural changes through the MCP tools.
- Manually edited edge ports and waypoints are intentionally regenerated from
  the schema and current table positions on the next save. Table geometry is
  preserved; route geometry is deterministic rather than a round-trip input.
- A foreign or metadata-free draw.io document is parsed from its visible native
  table shapes on a best-effort basis. Names, types, common key markers, and
  row-anchored relations can be recovered. A table-to-table edge without MCP
  endpoint attributes is skipped because its columns cannot be inferred
  safely. Comments, arbitrary styling, some constraint details, and complex
  edge semantics may be lost. The next save regenerates the document in the
  server's standard style.
- The router separates shared lanes and avoids table interiors, but a dense ERD
  can still contain line crossings. Crossings use arc jumps, and relation
  labels have opaque backgrounds. Labels can be dragged in draw.io when final
  presentation polish matters.

## Tests

Run the full suite from any directory:

```bash
uv run --project /absolute/path/to/MCP-Draw.IO \
  --locked --no-sync pytest
```

The suite should cover all input sources, composite-primary-key cardinality,
granular tools, save/open behavior, and draw.io round-trips both with and
without embedded metadata.

The real stdio subprocess test is opt-in because restricted sandboxes can block
AnyIO worker threads used by the SDK transport:

```bash
MCP_STDIO_INTEGRATION=1 uv run --locked --no-sync \
  pytest tests/test_stdio.py
```

## Desktop validation

XML parsing and server round-trip tests do not prove that draw.io Desktop lays
out and edits every native shape correctly. Before a release, generate a
representative ERD containing PK, FK, combined `PF`, unique, nullable,
one-to-one, one-to-many, and composite-key cases.

First exercise the real Desktop renderer non-interactively:

```bash
drawio --export --format png --border 20 \
  --output validation.png validation.drawio
```

On Windows PowerShell, replace `drawio` with the full path to `draw.io.exe`.
Then open `validation.drawio` in the desktop application and confirm:

- table headers and rows render without clipping or overlap;
- `PK`, `FK`, `PF`, and `U` markers are visible;
- ERD endpoints and cardinality markers are correct;
- single-column relations attach to their column rows;
- every relationship can be traced independently from endpoint to endpoint;
- parallel relationships use separate ports and lanes rather than sharing a
  segment;
- recursive relationships loop outside the table;
- crossings have visible arc jumps and no route crosses a table interior;
- tables can be selected, moved, resized, collapsed, and expanded;
- the hidden metadata cell is not visible; and
- saving with compression disabled produces a file `open_diagram` can reopen.

TDQS

A3.5/5.0

Scored across 16 tools

Disambiguation5/5

Each tool targets a distinct action and resource: loading schemas from different sources, opening/listing/describing diagrams, and CRUD operations for tables, columns, and relations. Manual positioning and automatic relayout are clearly separated, so there is no ambiguity between tools.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in lowercase snake_case (e.g., load_database_schema, add_table, remove_relation). Minor pluralization in list_diagrams is the only deviation, and it does not affect the predictable pattern.

Tool Count4/5

At 16 tools, the set is slightly above the typical 3-15 range, but each tool serves a distinct and necessary purpose in the schema-diagram workflow. The count feels well-scoped without redundant tools, though it could be tightened slightly.

Completeness4/5

The server covers the full lifecycle: loading/opening diagrams, listing/describing them, adding/removing tables/columns/relations, positioning, saving, and exporting. Minor gaps exist, such as no update/rename operations for tables or columns and no explicit close-diagram tool, but these are workable around.

Maintenance

ActivitySlowing
ResponsivenessNo issues