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_linedraw_circlequery_entitiesset_entity_layerdelete_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:
- Extracting a BOM from a table drawn entirely with lines and text.
- Extracting information from a drawing frame and title block.
- Splitting a drawing containing multiple framed sheets into independent DXF files.
All three worked.
Let's see how.
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.
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
Think of it as handing the AI a workbench.
It can inspect the drawing:
cad.drawing
cad.entities
cad.tables
cad.blocks
cad.selection
It can measure geometry:
cad.measure
It can edit the drawing:
cad.edit.transaction
And it can save the result:
cad.file.saveDxf
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:
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
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)
)
])
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
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"
]
}
}
}
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"
]
}
}
}
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"
}
You can also pass an HTTP or HTTPS URL.
If you want to watch the agent work:
{
"path": "./plan.dxf",
"mode": "write",
"visible": true
}
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)
)
])
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')
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()
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
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
}))
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)
}
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)
)
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
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]
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)
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"
}
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
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:
- Find each frame.
- Keep the entities belonging to that frame.
- Remove everything else.
- 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
)
Then it selected the second frame as an example:
const box = frames[1].extents
Next, it collected the entities inside the frame:
const inside = cad.entities
.inBox(box.min, box.max)
.map(e => e.id)
And identified everything outside:
const outside = cad.entities
.where(
e => !inside.includes(e.id)
)
.map(e => e.id)
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
}
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.dxfsheet-02.dxfsheet-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:
- Repository: github.com/mlightcad/cad-viewer
- MCP server on npm: @mlightcad/cad-mcp-server
- Script API documentation: mlightcad Script API
- The original design discussion: Building a CAD Agent: MCP, Tools, or Let AI Write Code?
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)