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 shopp

A few weeks ago, I published Building a CAD Agent: MCP, Tools, or Let AI Write Code?.
For complex software like CAD, giving AI a scripting API is often better than giving it a shopping list of tools.
If you want an AI agent to work with a CAD drawing, you could give it tools like:
"Find all circles near standard bolt-hole sizes, ignore reference layers, group them by component, flag unusual ones, and generate a report."
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.
mlightcad, our open-source browser-based CAD viewer, now supports the Model Context Protocol through @mlightcad/cad-mcp-server.
And then we tested it on three real drawing-processing tasks:
The first thing we wanted to avoid was building three separate AI implementations.
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.
This is where the actual CAD intelligence lives.
cad Enter fullscreen mode Exit fullscreen mode Think of it as handing the AI a workbench.
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.
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.
Currently, it contains exactly one tool:
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.
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.
So the in-app agent and the script panel share the same underlying capabilities.
This is the part that lets other AI applications work with mlightcad.
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.
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.
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.
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.
"Find all circles smaller than 5, group them by layer, and report their IDs and radii."
find_small_circles_grouped_by_layer_and_report_radius?
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.
Instead of teaching the AI every possible combination, we let it combine existing capabilities.
// 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.
And the result comes back as structured data.
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 model gets the information it actually needs.
But scripts can check their own results.
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.
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.
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:
The other handles everything the agent wants to do with the drawing.
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.
Every task follows the same basic pattern.
{ "path": "./plan.dxf", "mode": "write" } Enter fullscreen mode Exit fullscreen mode You can also pass an HTTP or HTTPS URL.
{ "path": "./plan.dxf", "mode": "write", "visible": true } Enter fullscreen mode Exit fullscreen mode Step 2: Execute a script
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.
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.
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.
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.
In CAD, a table doesn't necessarily exist as a table object.
Sometimes it's just a collection of lines and text.
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.
Extract the BOM and return it as a Markdown table.
The agent wrote a short script to reconstruct the table.
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 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.
Production drawings usually contain a frame and title block.
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.
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.
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.
{ "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.
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.
Some drawings contain multiple independent sheets inside one file.
For example, a drawing might contain three framed sheets arranged side by side.
In other words, turn one drawing into several drawings.
This time, the task involved editing the drawing.
The agent used cad.edit.transaction to group the changes into one undoable operation.
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.
Because we don't want the second export to start from an already modified drawing.
Each sheet should be extracted from the pristine source.
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.
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.
If the information is represented by text and geometry, the agent can often reconstruct its structure.
The model can write scripts that inspect the drawing and return structured results.
Instead of editing entities one at a time, the agent can execute a batch script.
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.
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.
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.
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.
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.