Skip to content

Command Line

Two command-line programs ship alongside the application. Both use the same core, the same node library and the same export path as the app, so a headless run reproduces what the Export dialog would have written.

Opens a project, evaluates it, writes its textures and exits. No window, no Flutter engine.

surface_lab_headless <project.surfacelabs> --out <dir>
[--native-lib <path to surface_gpu>]
[--format png|tga|tiff|bmp|exr]
[--prefix <name>]
[--material <index>|all]
[--verbose]
FlagMeaning
--out, -oOutput directory. Required.
--native-libPath to the surface_gpu library. Auto-detected from a source checkout; SURFACE_GPU_LIB also works.
--format, -fOutput format: png, tga, tiff (tif also accepted), bmp or exr. Anything else is an error rather than a fallback. Defaults to the project’s saved export preset.
--prefix, -pFile-name prefix. Defaults to the saved preset, otherwise a slug of the project name.
--material, -mA material index, or all. Defaults to the active material. all exports every material, suffixing files with the material name.
--verbose, -vPrint core diagnostics to stderr.
--help, -hUsage.

It prints what it opened, each file it wrote, and a summary. Warnings, meaning load warnings and skipped nodes, go to stderr.

Exit codes: 0 wrote at least one file, 1 usage or project error, 2 no render core, 3 nothing to export.

Trim Sheets export from here like any other output node: the strip compositor is an ordinary fragment shader, so the whole sheet resolves on the native core with no app involved. Draw nodes, Text nodes and Custom Shaders the native path declined are still skipped, each named on stderr.

This is the piece you want in CI. A build step can re-export a committed .surfacelabs on every change with no GUI, no display and no human.

The same nine tools as the in-app server, driving projects on disk instead of the open one. project create and project close both work here. See MCP Server for what the tools do; the flags below are what differs.

surface_lab_mcp [--http [port]] [--token <secret>]
[--native-lib <path>] [--out <dir>]
[--root <dir>] [--read-only] [--log <file>] [--verbose]
FlagMeaning
(none)JSON-RPC over standard input and output, the transport most MCP clients launch a server with. This is the default.
--http [port]Serve the same protocol on a loopback port instead, default 4319, like the in-app server.
--token <secret>Bearer token required on HTTP requests. Without it, --http accepts any local caller.
--native-lib <path>surface_gpu library path. Auto-detected in a source checkout; SURFACE_GPU_LIB also works.
--out <dir>, -oDefault directory for renders and exports when a tool names none. Defaults to <temp>/surface_lab_mcp.
--root <dir>Confine every file argument to this directory, rejecting .. traversal out of it. Repeatable. Without it the server can read and write anywhere you can.
--read-onlyRefuse mutating tools. See the caveat below.
--log <file>Append diagnostics to a file instead of stderr.
--verbose, -vInclude core diagnostics.
--help, -hUsage, on stderr.

A stdio client config:

{
"mcpServers": {
"surface-labs": {
"command": "dart",
"args": ["run", "surface_lab_mcp", "--out", "C:/textures/out"],
"cwd": "C:/…/surface_lab/packages/surface_lab_mcp"
}
}
}

On stdio, standard output is the protocol. Every diagnostic goes to stderr or the log file so nothing corrupts the stream, and --http prints its endpoint to stderr for the same reason.

On stdio the server exits when its client closes the pipe. Under --http it serves until interrupted, and exits 4 if the port will not bind.

Today the CLI treats the project as a closed box. Every input and every parameter is whatever was saved, and the only knobs are output format, file-name prefix and which material. Batching means running the CLI once per project in a shell loop, or exporting --material all.

graph validate is cheap and needs no GPU, so run it before a long export. See MCP Server for which node types the headless renderer skips and what happens when the native core is missing.