DEV Community

Mlight Lee
Mlight Lee

Posted on

Letting AI Write Code Against Your Drawings: The mlightcad MCP Server

Stop teaching AI 100 CAD tools. Give it a scripting API and let it figure things out.

A few weeks ago, I published Building a CAD Agent: MCP, Tools, or Let AI Write Code?.

The basic argument was simple:

For complex software like CAD, giving AI a scripting API is often better than giving it a shopping list of tools.

Think about it.

If you want an AI agent to work with a CAD drawing, you could give it tools like:

  • draw_line
  • draw_circle
  • query_entities
  • set_entity_layer
  • delete_entity

Sounds reasonable, right?

Until someone asks:

"Find all circles near standard bolt-hole sizes, ignore reference layers, group them by component, flag unusual ones, and generate a report."

Oops. Looks like we need another tool.

And another.

And another.

At some point, your MCP server starts looking like a restaurant menu with 500 dishes. The AI is still hungry, and you're still adding pages.

So we tried something different.

mlightcad, our open-source browser-based CAD viewer, now supports the Model Context Protocol through @mlightcad/cad-mcp-server.

Instead of exposing every CAD operation as a separate tool, we give the agent a scripting API and let it write JavaScript to perform the actual work.

And then we tested it on three real drawing-processing tasks:

  1. Extracting a BOM from a table drawn entirely with lines and text.
  2. Extracting information from a drawing frame and title block.
  3. Splitting a drawing containing multiple framed sheets into independent DXF files.

All three worked.

Let's see how.

mlightcad mcp


1. The architecture: One brain, multiple ways to use it

The first thing we wanted to avoid was building three separate AI implementations.

One for the in-app chat agent.

One for MCP.

And another one for whatever integration comes next.

That would be a great way to create three bugs for every bug we fix.

Instead, mlightcad uses a shared agent core.

mlightcad agent architecture

The architecture has three main parts.

@mlightcad/cad-agent-core — The engine

This is where the actual CAD intelligence lives.

It provides two capabilities.

1. A scripting environment

The agent can execute JavaScript against a live CAD drawing.

The script receives a single argument:

cad
Enter fullscreen mode Exit fullscreen mode

Think of it as handing the AI a workbench.

It can inspect the drawing:

cad.drawing
cad.entities
cad.tables
cad.blocks
cad.selection
Enter fullscreen mode Exit fullscreen mode

It can measure geometry:

cad.measure
Enter fullscreen mode Exit fullscreen mode

It can edit the drawing:

cad.edit.transaction
Enter fullscreen mode Exit fullscreen mode

And it can save the result:

cad.file.saveDxf
Enter fullscreen mode Exit fullscreen mode

For example, the agent can inspect entities, query layers, find objects inside a bounding box, measure distances, or modify geometry.

It doesn't need a separate MCP tool for every operation.

It just needs to know how to use the API.

Of course, letting an AI execute arbitrary JavaScript inside a CAD application sounds like an excellent way to spend your weekend debugging security problems.

So the scripts run inside a sandbox.

The validator restricts the available API. Scripts cannot import arbitrary modules or access the browser page.

The model gets access to the CAD workbench, not the entire house.

The complete API is documented here:

mlightcad Script API

2. A tool interface

The core also provides a tool catalog.

Currently, it contains exactly one tool:

execute_cad_script

The script runs inside the CAD environment.

If it produces a DXF file or a preview image, the core can attach those results to the tool response.

The important detail is that the model doesn't need to receive the entire drawing or the complete contents of the exported file.

It can receive a summary and file metadata instead.

We'll come back to why that matters.

@mlightcad/cad-agent-plugin — The in-app agent

This is the agent users interact with inside cad-viewer and cad-simple-viewer.

It registers the agent chat command and lets users describe drawing tasks in natural language.

The plugin uses the same core.

So the in-app agent and the script panel share the same underlying capabilities.

No separate implementation required.

@mlightcad/cad-mcp-server — MCP for external agents

This is the part that lets other AI applications work with mlightcad.

It supports MCP clients such as:

  • Claude Code
  • Claude Desktop
  • Cursor
  • Codex
  • Other compatible MCP clients

The server uses stdio and launches Chromium through Playwright when the first tool call arrives.

It runs the same cad-simple-viewer kernel used by our products.

The browser session persists across tool calls.

In headless mode, it automatically closes after an idle timeout. In visible mode, you can watch the agent manipulate the drawing.

The server exposes two tools:

Tool What it does
open_drawing Opens a local or remote DXF/DWG file, or starts with a blank ISO template
execute_cad_script Executes JavaScript against the current drawing

That's it.

One tool to open the drawing.

One tool to do the actual work.

WebMCP — The next frontend

We also plan to support WebMCP.

Because the MCP tool registration is already separated into a reusable registerCadMcpTools() function, a future WebMCP host can register the same tools inside a browser environment that already owns the CAD document manager.

No need to launch another Chromium instance.

Same core. Same tools. Another frontend.

That's the advantage of this architecture: new capabilities are implemented once and become available to every frontend.


2. Why scripts beat a pile of MCP tools

Let's get back to the original question.

Why not simply expose every CAD operation as an MCP tool?

Because CAD is not a small collection of predictable operations.

It's a programming environment.

Users can combine geometry, layers, blocks, text, measurements, and editing operations in almost unlimited ways.

Trying to anticipate every possible workflow is a losing game.

The tool-sprawl problem

Imagine we start with 20 tools.

Then users ask for:

  • Finding circles with specific radii.
  • Grouping entities by layer.
  • Comparing dimensions.
  • Extracting text from a particular region.
  • Finding duplicate geometry.
  • Exporting selected entities.

We add more tools.

Then someone asks:

"Find all circles smaller than 5, group them by layer, and report their IDs and radii."

Do we create:

find_small_circles_grouped_by_layer_and_report_radius?

Hopefully not.

The problem is that every new workflow becomes another API design and implementation task.

And the more tools we expose, the more information the model has to process before it even starts working.

The scripting alternative

Instead of teaching the AI every possible combination, we let it combine existing capabilities.

For example:

// Find circles smaller than radius 5,
// grouped by layer.

const results = cad.entities
  .where(
    e =>
      e.type === 'Circle' &&
      e.geometry.radius < 5
  )
  .map(e => ({
    handle: e.id,
    layer: e.layer,
    r: e.geometry.radius
  }))

return results
Enter fullscreen mode Exit fullscreen mode

That's the entire workflow.

The model writes the logic.

The CAD environment executes it.

And the result comes back as structured data.

No special-purpose tool required.

Three advantages that matter more than they first appear

1. Fewer tokens

Suppose a drawing contains 100,000 entities.

If the agent has to inspect them individually through tool calls, things get expensive very quickly.

With scripting, the filtering and processing happen inside the CAD environment.

The model receives only the relevant results.

Instead of shipping 100,000 entities back and forth, we can return a small JSON summary.

The drawing stays where it belongs.

The model gets the information it actually needs.

2. Easier verification

AI-generated code can make mistakes.

That's not exactly breaking news.

But scripts can check their own results.

For example:

return cad.measure.report([
  cad.measure.check(
    'radius',
    100,
    cad.measure.radius(id)
  )
])
Enter fullscreen mode Exit fullscreen mode

The agent can perform an operation, inspect the result, and verify whether the expected conditions hold.

We can also separate read-only analysis from drawing modifications.

This makes it easier to build workflows that are not just intelligent, but reasonably trustworthy.

3. Faster execution

Loops, filtering, and batch edits execute inside the CAD environment.

The model doesn't need to make a new tool call for every entity.

Think of the difference between asking someone to inspect 10,000 objects one by one and giving them a program that checks all 10,000 objects in one pass.

Let the model decide what to do.

Let the program do the repetitive work.

So, is MCP still useful?

Absolutely.

MCP is still the connection between the AI agent and external software.

Explicit tools are still useful for lifecycle operations, such as opening a drawing.

Scripts are useful for combining CAD capabilities into custom workflows.

That's why our MCP server has two tools:

One handles the drawing lifecycle.

The other handles everything the agent wants to do with the drawing.


3. Getting started with the mlightcad MCP server

Let's make this practical.

You need:

  • Node.js 20 or later.
  • A Playwright Chromium installation.

Install the browser using:

pnpm --filter @mlightcad/cad-mcp-server install-browser
Enter fullscreen mode Exit fullscreen mode

Claude Code / Claude Desktop

Add the following configuration to your MCP settings:

{
  "mcpServers": {
    "cad": {
      "command": "npx",
      "args": [
        "-y",
        "--package=@mlightcad/cad-mcp-server@1.7.8-preview.1",
        "cad-mcp-server"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Cursor

The configuration is essentially the same:

{
  "mcpServers": {
    "cad": {
      "command": "npx",
      "args": [
        "-y",
        "--package=@mlightcad/cad-mcp-server@1.7.8-preview.1",
        "cad-mcp-server"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Inside the mlightcad monorepo, use the bundled scripts/run-published.mjs launcher instead of raw npx.

See the MCP server README for details.

A couple of useful environment variables

Variable Purpose
CAD_MCP_VISIBLE=1 Show a visible Chromium window by default
CAD_MCP_IDLE_CLOSE_MS Configure the headless idle timeout; default is 30,000 ms

The typical workflow

Every task follows the same basic pattern.

Step 1: Open a drawing

Call open_drawing:

{
  "path": "./plan.dxf",
  "mode": "write"
}
Enter fullscreen mode Exit fullscreen mode

You can also pass an HTTP or HTTPS URL.

If you want to watch the agent work:

{
  "path": "./plan.dxf",
  "mode": "write",
  "visible": true
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Execute a script

Here's a small example.

Let's draw a circle with a radius of 100 and verify that it was created correctly.

let id

cad.edit.transaction('circle', tx => {
  id = tx.drawCircle({
    center: { x: 0, y: 0 },
    radius: 100
  })
})

cad.view.zoomExtents()

return cad.measure.report([
  cad.measure.check(
    'radius',
    100,
    cad.measure.radius(id)
  )
])
Enter fullscreen mode Exit fullscreen mode

The script creates the circle, adjusts the view, and checks the radius.

One call.

No draw_circle tool.

No measure_circle tool.

No verify_circle tool.

Just the scripting API.

Saving the result

To save the drawing:

cad.file.saveDxf('./plan-out.dxf')
Enter fullscreen mode Exit fullscreen mode

The MCP server writes the file and returns metadata such as its path and size.

The DXF contents are not sent back to the model.

This is important when working with large drawings.

You don't want the model to spend its context window reading a file it just created.

You want it to know the file exists and whether the operation succeeded.

You can also capture a preview:

cad.view.capturePreview()
Enter fullscreen mode Exit fullscreen mode

The result can be attached as an image to the MCP response.

So the agent can inspect the drawing visually when necessary.

That's the basic workflow.

Open the drawing.

Write a script.

Execute it.

Check the result.


4. Three real experiments

Now for the interesting part.

I tested the MCP server on three drawing-processing tasks.

These weren't three custom tools built specifically for the examples.

They used the same two-tool MCP server and the existing CAD scripting API.

The agent had to figure out how to combine the available capabilities.

4.1 Turning a line-drawn BOM into a Markdown table

Example 1 — table extraction

The task

In CAD, a table doesn't necessarily exist as a table object.

Sometimes it's just a collection of lines and text.

To a human, it looks like this:

Item Part No Description Qty Material
1 GP-001 Housing 1 HT250
2 GP-002 Cover 1 HT250
3 GP-003 Shaft 1 45 Steel
4 GP-004 Bolt M8×30 4 8.8
5 GP-005 Base Plate 1 HT200

To software, however, it's a bunch of geometry.

There may be lines forming the grid, text inside each cell, and numbered balloons connecting the BOM to the assembly.

I used a mechanical assembly drawing of a gear pump.

The assembly view occupied most of the sheet, while the BOM sat above the title block.

The task was simple:

Extract the BOM and return it as a Markdown table.

How the agent solved it

There was no extract_bom tool.

The agent wrote a short script to reconstruct the table.

First, it collected text entities:

const texts = cad.entities
  .where(e =>
    ['Text', 'MText', 'Attribute']
      .includes(e.type)
  )
  .map(e => ({
    text: e.geometry.text,
    pos: e.geometry.position
  }))
Enter fullscreen mode Exit fullscreen mode

Next, it grouped text by approximate Y coordinates.

Entities on the same horizontal row should have similar Y values.

const rows = new Map()

for (const t of texts) {
  const key = Math.round(t.pos.y / 10)

  if (!rows.has(key)) {
    rows.set(key, [])
  }

  rows.get(key).push(t)
}
Enter fullscreen mode Exit fullscreen mode

Then it sorted the rows from top to bottom and the text within each row from left to right:

return [...rows.values()]
  .sort(
    (a, b) =>
      b[0].pos.y - a[0].pos.y
  )
  .map(row =>
    row
      .sort(
        (a, b) =>
          a.pos.x - b.pos.x
      )
      .map(t => t.text)
  )
Enter fullscreen mode Exit fullscreen mode

Finally, the script returned structured rows.

The model converted those rows into Markdown.

The result

The BOM was reconstructed as a Markdown table.

The interesting part wasn't that the AI could read text.

It was that the AI figured out how to reconstruct a table that didn't exist as a table object.

The grouping rules were generated on the fly.

If the row spacing or column arrangement was different, the agent could adjust the script and run it again.

Nobody on the development team had to anticipate that particular table layout.

And only the extracted data was returned to the model.

The drawing itself stayed inside the CAD environment.

That's the flexibility we were looking for.


4.2 Reading a drawing frame from top to bottom

Example 2 — frame extraction

The task

Production drawings usually contain a frame and title block.

The title block may contain:

  • Project name.
  • Drawing title.
  • Drawn-by and checked-by information.
  • Scale.
  • Date.
  • Sheet number.
  • Revision.

Humans can read this information easily.

But to a CAD program, it's just a collection of entities.

I asked the agent to extract all the information from the drawing frame and organize it into a structured record.

How the agent solved it

The script followed three steps.

First, locate the frame.

For this drawing, the agent used the largest closed polyline as the frame.

const frame = cad.entities
  .where(
    e =>
      e.type === 'Polyline' &&
      e.geometry.closed
  )
  .sort(
    (a, b) =>
      (b.extents.max.x - b.extents.min.x) *
      (b.extents.max.y - b.extents.min.y) -
      (a.extents.max.x - a.extents.min.x) *
      (a.extents.max.y - a.extents.min.y)
  )[0]
Enter fullscreen mode Exit fullscreen mode

Second, collect the entities inside the frame.

The API provides inBox() for this:

return cad.entities
  .inBox(
    frame.extents.min,
    frame.extents.max
  )
  .filter(
    e =>
      ['Text', 'MText', 'Attribute']
        .includes(e.type)
  )
  .map(e => e.geometry.text)
Enter fullscreen mode Exit fullscreen mode

Block attributes are included automatically.

That means title blocks built from attributed block inserts can be processed without special handling.

Third, the model organizes the extracted text into a structured object.

The result

{
  "project": "PUMP STATION UPGRADE",
  "title": "PUMP DISCHARGE PIPING",
  "drawnBy": "J. Chen",
  "checkedBy": "L. Wang",
  "scale": "1:50",
  "date": "2026-10-12",
  "sheetNo": "A-102",
  "rev": "B"
}
Enter fullscreen mode Exit fullscreen mode

The drawing geometry was never modified.

This was a read-only operation.

And because the output is structured JSON, it can go directly into a document-management database, search index, or CSV export.

No need to manually read title blocks anymore.

At least, not for this workflow.


4.3 Splitting a multi-sheet drawing into separate DXF files

Example 3 — split by frame

The task

Some drawings contain multiple independent sheets inside one file.

For example, a drawing might contain three framed sheets arranged side by side.

I asked the agent to:

  1. Find each frame.
  2. Keep the entities belonging to that frame.
  3. Remove everything else.
  4. Export the remaining drawing as an independent DXF.

In other words, turn one drawing into several drawings.

How the agent solved it

This time, the task involved editing the drawing.

The agent used cad.edit.transaction to group the changes into one undoable operation.

First, it located the frames:

const frames = cad.entities
  .where(
    e =>
      e.type === 'Polyline' &&
      e.geometry.closed
  )
Enter fullscreen mode Exit fullscreen mode

Then it selected the second frame as an example:

const box = frames[1].extents
Enter fullscreen mode Exit fullscreen mode

Next, it collected the entities inside the frame:

const inside = cad.entities
  .inBox(box.min, box.max)
  .map(e => e.id)
Enter fullscreen mode Exit fullscreen mode

And identified everything outside:

const outside = cad.entities
  .where(
    e => !inside.includes(e.id)
  )
  .map(e => e.id)
Enter fullscreen mode Exit fullscreen mode

Finally, it removed the unwanted entities and saved the result:

cad.edit.transaction(
  'isolate sheet 2',
  tx => {
    tx.erase(outside)
  }
)

cad.file.saveDxf('./sheet-02.dxf')

return {
  kept: inside.length,
  removed: outside.length
}
Enter fullscreen mode Exit fullscreen mode

The agent then reopened the original drawing and repeated the process for the other frames.

Why reopen the original?

Because we don't want the second export to start from an already modified drawing.

Each sheet should be extracted from the pristine source.

The result

The agent produced:

  • sheet-01.dxf
  • sheet-02.dxf
  • sheet-03.dxf

Each file contained one frame and its associated entities.

The MCP server returned file metadata, including paths and sizes.

The agent could verify that all three exports were non-empty before reporting success.

The entire workflow was assembled from existing API operations.

No one had to write a dedicated multi-sheet splitting tool.

The agent combined entity queries, bounding boxes, transactions, and DXF export into a batch-processing workflow.

That's a pretty useful capability for something we didn't explicitly program as a feature.


5. What can you actually do with this?

The three experiments demonstrate something broader than extracting a BOM.

Once the agent can query, analyze, edit, and export CAD data through scripts, many drawing-processing tasks become combinations of existing capabilities.

Here are some examples.

Information extraction

Extract information from:

  • BOMs.
  • Title blocks.
  • Schedules.
  • Legends.
  • Notes.

If the information is represented by text and geometry, the agent can often reconstruct its structure.

Drawing analysis

Generate reports such as:

  • Entity counts by layer.
  • Duplicate or overlapping geometry.
  • Dimension checks.
  • Unusual geometry.
  • Drawing statistics.

The model can write scripts that inspect the drawing and return structured results.

Batch editing

Perform operations such as:

  • Renaming layers.
  • Moving groups of entities.
  • Fixing linetypes.
  • Inserting standard blocks.

Instead of editing entities one at a time, the agent can execute a batch script.

File processing

Automate tasks such as:

  • Splitting drawings by frame.
  • Merging sheets.
  • Converting drawings.
  • Exporting independent DXF files.

Visual verification

The agent can open a visible browser window and let you watch the drawing being manipulated.

It can also capture a preview image for inspection.

The point is not that every task will work perfectly on the first attempt.

The point is that the agent can compose existing capabilities into workflows that developers never explicitly implemented.

And when the API gets better, the agent gets better too.

Every new method added to cad-agent-core becomes available to the in-app agent, the MCP server, and the planned WebMCP host.

No need to build a new tool for every new idea.


6. The bigger picture: Give AI a workbench, not a menu

We started with a familiar question:

What tools should I expose so AI can use my software?

Our answer is becoming a little different.

Instead of trying to enumerate every possible operation, give AI a documented scripting API, a controlled execution environment, and a small number of tools for connecting to the application.

In mlightcad, that means:

  • A shared agent core.
  • A sandboxed JavaScript API.
  • One tool for opening drawings.
  • One tool for executing scripts.
  • Structured results and optional visual previews.

The agent can inspect a drawing, generate code, execute it, verify the result, and save the output.

And developers don't need to anticipate every possible combination of CAD operations.

We still need explicit tools.

We still need MCP.

But we don't need to turn every CAD API method into a separate tool.

Sometimes the best interface for AI isn't a bigger toolbox.

It's a workbench where the AI can write its own instructions.


Try it yourself

The mlightcad MCP server is available here:

The MCP server currently uses the preview package version 1.7.8-preview.1.

If you're building an AI agent for CAD, I'd be interested to hear how you're approaching the interface.

Are you building a tool for every operation?

Or are you giving the agent a scripting environment and letting it compose the operations itself?

Because in our experience, the second approach gets interesting very quickly.

And it saves us from maintaining a 500-item tool menu.

Which, frankly, is a feature I don't think anyone wants to use.

Top comments (0)