{"id":"c3a0ef55-8bd2-47d3-ab40-175d33db7c48","slug":"uncad","kind":"mcp","name":"uncad","summary":"Rust library for parsing, rendering (SVG/PNG), and writing CAD DWG/DXF files — safe FFI bindings over LibreDWG.","owner":"iyulab","repoUrl":"https://github.com/iyulab/uncad","license":"GPL-3.0","licenseTier":"strong-copyleft","trust":"community","stars":1,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-08-14T08:22:56.000Z","addedAt":"2026-10-21T21:44:56.603Z","logoUrl":"https://avatars.githubusercontent.com/u/52388508?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["design-media","files-documents","writing"],"tags":["autocad","cad","dwg","dxf","ffi","libredwg","rust","svg"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-26T23:36:38.364Z","sample":false,"url":"https://conduits.dev/tools/uncad","language":"Rust","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# uncad\n\n[![CI](https://github.com/iyulab/uncad/actions/workflows/ci.yml/badge.svg)](https://github.com/iyulab/uncad/actions/workflows/ci.yml)\n[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)\n\nAn open-source Rust library that parses CAD files (DWG/DXF) into a model and\nexports that model as JSON, SVG or PNG. Read-only: it does not write DWG or DXF,\nand does not convert between them.\n\n## Quick start\n\n```bash\ngit submodule update --init   # test fixtures only -- not needed to build (see \"Platform\")\ncargo build --workspace\ncargo test --workspace\n```\n\n```rust\nlet db = uncad::parse(\"drawing.dwg\")?;   // same model for DWG and DXF (entities + tables)\nprintln!(\"{} entities\", db.entities.len());\n\n// The header variables that give the numbers a meaning come beside the model:\nlet (db, header) = uncad::parse_with_header(\"drawing.dxf\")?;\nprintln!(\"units: {:?}\", header.units());   // $INSUNITS; None when the file does not say\n\n// Bytes already in memory (from the network, an archive, ...):\nlet bytes = std::fs::read(\"drawing.dwg\")?;\nlet db = uncad::parse_bytes(&bytes, uncad::Format::Dwg)?;\n\nlet json = db.to_json(uncad::ToJsonOptions { pretty: true })?;   // the model, serialized as-is\nstd::fs::write(\"drawing.json\", json)?;\n\n// Rendering is the iron-render-cad crate's (uncad-cli uses it):\nlet result = iron_render_cad::to_svg(&db, iron_render_cad::ToSvgOptions::default());\nstd::fs::write(\"drawing.svg\", result.svg)?;\n```\n\n## CLI\n\nThe binary is `uncad`. From a clone, `cargo install --path crates/uncad-cli`\nputs it on your `PATH`; the lines below run it through `cargo run` instead.\n\n```bash\ncargo run -p uncad-cli -- drawing.dwg                            # summary: entity count per type\ncargo run -p uncad-cli -- drawing.dwg -o drawing.json --pretty   # export the parsed model\ncargo run -p uncad-cli -- drawing.dwg -o drawing.svg             # render to SVG (model space)\ncargo run -p uncad-cli -- drawing.dwg -o drawing.png             # render to PNG (via SVG)\ncargo run -p uncad-cli -- drawing.dwg -o drawing.png --scale 2   # rasterize at twice the size\ncargo run -p uncad-cli -- drawing.dwg -o drawing.png --fit 4000  # the longer side 4000 px\ncargo run -p uncad-cli -- drawing.dwg -o drawing.png --fit 4000 --stroke 2 --paper dark  # 2 px lines, black page\ncargo run -p uncad-cli -- drawing.dwg -o detail.svg --window 0,0,500,300  # just this rectangle (drawing units)\ncargo run -p uncad-cli -- drawing.dwg -o drawing.svg --no-trim   # draw far-away outliers too\ncargo run -p uncad-cli -- drawing.dwg -o sheet.svg --space paper # sheet borders / title blocks\ncargo run -p uncad-cli -- drawing.dwg -o all.svg --space all     # every space in one document\ncargo run -p uncad-cli -- export drawing.dwg -o pkg/             # images + JSON an LLM or a vision model reads\n```\n\nA render is drawn on a white page by default, where pure white (ACI 7) is drawn\nblack. `--paper dark` draws it on a black page instead: pure white stays white,\npure black is drawn white, and every other color is the file's -- the light ones\na white page washes out included. Lines are about 1/6000 of the picture's\ndiagonal unless `--stroke <px>` sets their width in pixels: pixels of the image\nin a PNG, pixels on the screen at any zoom in an SVG (a style rule with\n`vector-effect: non-scaling-stroke`).\n\nQuestions about a drawing, each answered as one line of JSON -- the result\nstructure of [`iron-scout-cad`](https://github.com/iyulab/iron-scout-cad) or\n[`iron-diff-cad`](https://github.com/iyulab/iron-diff-cad), serialized as it is:\n\n```bash\nuncad summarize drawing.dwg                                   # what the drawing contains\nuncad summarize drawing.dwg --type circle --layer pipes       # ... and the circles on a layer, each with its box\nuncad summarize drawing.dwg --id 812 --detail                 # ... and one entity's model record (the fields `set` takes)\nuncad hit-test drawing.dwg --x 120 --y 45 --tolerance 0.5     # the entities at a point\nuncad relate drawing.dwg                                      # what each dimension's definition points sit on\nuncad diff rev-a.dwg rev-b.dwg                                # exact numeric difference (by reference ID when the files show one drawing)\nuncad diff a.dxf b.dxf --matching geometry --length-tolerance 0.01\nuncad diff a.dxf b.dxf --matching geometry --min-similarity 1.5  # ... pairing only equal shapes\nuncad diff rev-a.dwg rev-b.dwg --omit within,unstated         # only what changed beyond tolerance\n```\n\nA change set lists every field that differs, including those that moved within\ntolerance and those one drawing does not state (a drawing saved again in a newer\nformat states values the older format had no place for). `--omit` leaves either\nout -- for an agent reading the answer into a limited context -- and the answer\ncounts what it left out in `omitted`, so nothing left out reads as unchanged.\n\nOne verb edits: `set` changes one field of one entity and writes the result as a\nnew model JSON file -- never over an existing file, never into its input -- and\nanswers with the difference it made, as `diff` reports it. The field and the\nvalue are named as the model JSON names them (the value is JSON: a number, a\nstring in quotes, an object). Every verb reads model JSON as a drawing, so edits\nchain, and the last state can be compared with the first -- or rendered, since\nevery command but `export` (which reads the drawing's header) takes model JSON:\n\n```bash\nuncad set part.dwg --id 42 --path radius --value 6.5 -o step1.json\nuncad set step1.json --id 42 --path center.x --value 10 -o step2.json\nuncad diff part.dwg step2.json                                # both edits, and nothing else\nuncad step2.json -o step2.svg                                 # what the edited state looks like\n```\n\n`redline` draws that difference on top of the first drawing: the first drawing\nexactly as it renders alone, and over it, in one color, what each changed entity\nbecame, what was removed (dashed), and a revision cloud around every change. A\nchange whose counterpart is uncertain gets a dashed cloud and no geometry. The\ncolor is the first of red, purple, blue, green and orange that the drawing\nitself does not come close to, and the answer says which (`proposal_color`);\n`--proposal-color` gives one instead, and when the drawing uses colors close to\nthe one given, the answer lists them and a warning suggests another. It\nwrites an SVG or PNG file -- never over an existing one -- and answers with what\nit marked and what it could not show, and why. `--paper` and `--stroke` mean what\nthey mean for a render: a stroke is pixels of the image in a PNG, and pixels on\nthe screen in an SVG:\n\n```bash\nuncad redline part.dwg step2.json -o proposal.svg\nuncad redline part.dwg step2.json -o proposal.png --fit 2000 --omit within  # 2000 px, beyond tolerance only\nuncad redline plan.dwg step2.json -o detail.png --fit 1200 --frame changes  # just the changes, and around them\nuncad redline plan.dwg step2.json -o proposal.svg --proposal-color '#0057b8'  # a color of your own\nuncad redline plan.dwg step2.json -o review.png --fit 1200 --stroke 3 --paper dark  # 3 px lines, black page\nuncad redline plan.dwg step2.json -o review.svg --stroke 1 --paper dark  # 1 px on screen at any zoom\n```\n\n`uncad mcp` serves `summarize`, `hit-test`, `relate`, `diff`, `set` and `redline` as [Model Context\nProtocol](https://modelcontextprotocol.io) tools over stdio, for an agent to\ncall. A tool's answer is byte for byte what the command prints; a warning the\ncommand writes to stderr is a further content block. Each call reads its files\nagain -- the server keeps no state -- and calls run one at a time.\n\n```json\n{ \"mcpServers\": { \"uncad\": { \"command\": \"uncad\", \"args\": [\"mcp\"] } } }\n```\n\n## Scope\n\n1. **DWG** — read through [LibreDWG](https://www.gnu.org/software/libredwg/)\n   (GPLv3+), bound directly via Rust FFI (`bindgen`). All versions.\n2. **DXF** — ASCII and binary, R2007 and later included, read by the pure-Rust\n   `undxf` crate (MIT) rather than by LibreDWG; chosen by file extension\n   (`parse`) or by the caller (`parse_bytes`). A drawing's DXF and DWG read to\n   the same model except where the two formats state different things\n   ([`docs/CAVEATS.md`](./docs/CAVEATS.md), \"DXF\").\n3. **Output** — the parsed model as JSON (`CadDatabase::to_json`, from\n   `uncad-model`). SVG and PNG come from the\n   [`iron-render-cad`](https://github.com/iyulab/iron-render-cad) crate (MIT), which\n   `uncad-cli` uses. Writing DWG/DXF is not offered; the write API that existed in\n   0.1.0 was removed (see [`CHANGELOG.md`](./CHANGELOG.md)).\n\nThe model itself -- `CadDatabase`, `Entity`, `Tables` and their JSON form -- is\nthe [`uncad-model`](https://github.com/iyulab/uncad-model) crate (MIT), re-exported\nhere as `uncad::model` / `uncad::tables` / `uncad::json`. This crate is one backend\nthat fills it; anything that only needs to *read* a drawing depends on the model\ncrate alone and inherits nothing from this crate's license.\n\n## Platform\n\nPure Rust plus native FFI. WebAssembly and the browser are not targets — the\nintended consumers are libraries and binaries (CLI, server, desktop app).\n\n`bindgen` needs `libclang`, so LLVM/Clang has to be installed (Windows:\n`winget install LLVM.LLVM`, Ubuntu: `apt install libclang-dev`). The LibreDWG C\nsources are vendored into `crates/libredwg-sys/vendor/libredwg/`, so **building\ndoes not need the `lib/libredwg` submodule**. Running `cargo test --workspace`\ndoes: the real-file tests under `crates/uncad/tests/` and `uncad-cli`'s\n`tests/documented_invocations.rs` read fixtures from that submodule's\n`test/test-data/`. Clone with\n`git clone --recurse-submodules`, or run `git submodule update --init` in an\nexisting clone. Builds and tests pass on Linux (`x86_64-unknown-linux-gnu`) as\nwell as Windows.\n\n## License\n\n**GPLv3-or-later**. LibreDWG (GPLv3+) is the only third-party component linked\nin, and its license carries over. The vendored copy carries local patches;\n`crates/libredwg-sys/NOTICE.md` lists each one and is their modification\nnotice, inside the crate so that it reaches the published tarball. Copyright and license details for\nthird-party components are in\n[`docs/THIRD_PARTY_NOTICES.md`](./docs/THIRD_PARTY_NOTICES.md).\n\n## Repository layout\n\n```\nlib/libredwg/            LibreDWG upstream, as a git submodule. Not used by the build --\n                         it is the source vendor/ is regenerated from, and where the\n                         real-file test fixtures (test/test-data/) come from\ncrates/\n  libredwg-sys/          raw FFI (cc + bindgen). vendor/libredwg/ holds the subset of C\n                         sources actually compiled (for publishing to crates.io), with\n                         local patches marked \"uncad local patch\" and listed in\n                         NOTICE.md; shim/ holds the C accessors for opaque types, and\n                         vendor-config/config.h stands in for autotools\n  uncad/                 the safe API: parse() / parse_bytes() -> uncad_model::CadDatabase,\n                         and the drawing's Header beside it from parse_with_header()\n  uncad-cli/             the CLI binary (uncad)\ncrates/*/tests/          integration tests against the public API. crates/*/examples/ are\n                         manual-check tools, and #[cfg(test)] blocks inside src/*.rs are\n                         unit tests -- docs/ARCHITECTURE.md's \"Test layout\" says which\n                         belongs where\nscripts/                 sync-libredwg-vendor.sh -- regenerates vendor/ after a submodule\n                         update\nsamples/                 gitignored except its README -- drop any DWG/DXF in here for\n                         manual testing, no license clearance needed. No automated tests\n                         read from it\ndocs/                    architecture, known limitations, third-party notices\n```\n\nFurther reading:\n\n- [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) — crate layout, build system,\n  test layout, the FFI/bindgen boundary, thread safety, the entity model\n- [`docs/CAVEATS.md`](./docs/CAVEATS.md) — entity type coverage, known\n  limitations and bugs, cross-platform notes\n"}