Appearance
Headless & MCP (drive Studio from an agent)
The scripting API runs inside a browser. The headless engine runs that same API with no browser of your own - so an AI agent, a CI job, or a shell script can build, mesh, solve, query and see a model.
A headless Chromium runs the real Studio (same WASM solver, same THREE.js viewport, same window.CivilKit / window.__fem), so nothing can diverge from what a person sees - and the screenshot tool returns the actual 3D viewport as an image. Callers get eyes, not just numbers.
Why browser-backed, not pure Node
The solver, the section geometry and the renderer are the same code the app ships. Running them in a headless browser means the agent path is always identical to the user path - and it can take a screenshot of exactly what you'd see on screen.
Requirements
- Node 18+
- Playwright with a Chromium build:
npm i -g playwright && npx playwright install chromium(or a local dependency).
From an MCP client (Claude Desktop, Cursor, Cline)
Add the server to your client's MCP config. For Claude Desktop that's claude_desktop_config.json:
json
{
"mcpServers": {
"civilkit": {
"command": "node",
"args": ["/ABS/PATH/TO/platform/mcp/server.mjs"],
"env": { "NODE_PATH": "/ABS/PATH/TO/npm-global/node_modules" }
}
}
}NODE_PATH lets the server find a globally-installed Playwright - set it to the output of npm root -g, or omit it if Playwright is a local dependency. Restart the client and the civilkit tools appear. The first tool call boots the headless browser (~5-10 s once); subsequent calls are fast.
The tools
| Tool | What it does |
|---|---|
load_sample / load_model | Load a built-in sample by key, or a full model object |
add_node / add_member / set_support | Build geometry + supports headlessly |
mesh_all | Mesh every detected bay into plates (plane: all / floor / wall, elementSize in m) |
solve | Run the analysis; returns a results summary |
get_reactions / summary / validate | Read results / model stats / model-health issues |
run_script | Escape hatch: run arbitrary JS in the page (CivilKit + fem in scope, return a value) |
screenshot | PNG of the 3D viewport (optional renderMode: solid | lines) - the "eyes" |
run_script is the full power: anything on window.CivilKit - including assignSection, windLoads, seismicLoads, loadCombinations and report - is callable from it, so the entire design workflow runs headlessly.
From a shell (no MCP client)
The same engine has a one-shot CLI:
sh
node platform/mcp/cli.mjs sample portalFrame # load a sample, print summary
node platform/mcp/cli.mjs solve model.json # load a model file, solve, print summary
node platform/mcp/cli.mjs mesh floor 3 # mesh all floor bays at 3 m elements
node platform/mcp/cli.mjs script "return CivilKit.summary()"
node platform/mcp/cli.mjs shot out.png lines # screenshot the viewport to a PNGExample: design a building headlessly
This is the office building from examples/office-building.mjs - generated, given real sections, loaded to AS/NZS, solved and checked, entirely through the engine's run_script (all one script):
js
// (inside run_script) - the model is already loaded
const F = window.__fem, C = window.CivilKit;
// classify + assign real catalogue sections
await C.assignSection(columnIds, '310UC158');
await C.assignSection(beamIds, '610UB101');
// AS/NZS code loads + combinations
C.windLoads({ region: 'A', terrain: '3' }); // 1170.2
C.seismicLoads({ zone: '0.08', soil: 'De' }); // 1170.4
C.loadCombinations(); // 7 AS/NZS 1170.0 combinations
const summary = await C.solve(); // solves every combination
return { checks: summary.memberCapacities, reportKB: Math.round(C.report().length / 1024) };Then screenshot returns the rendered, solved building - real I-section profiles, coloured by the deflection contour.
Files
platform/mcp/engine.mjs- the headless-Studio driver (static file server + Playwright + tool functions + screenshot). Shared by both entry points.platform/mcp/server.mjs- minimal MCP stdio server (newline-delimited JSON-RPC 2.0, no SDK dependency).platform/mcp/cli.mjs- one-shot shell wrapper over the same engine.
Security
run_script runs arbitrary JavaScript
run_script executes any JS in the page - it's a local automation surface, the same trust level as devtools on your own model. Don't expose this server to untrusted callers as-is. For a multi-tenant or remote deployment, put it behind the version/live Worker's auth model (a master token plus per-project scoped JWTs) and consider disabling run_script for scoped tokens.