Architecture¶
The AI-proposed design for the engine. It follows the requirements and is one candidate design, open to change. Tool API details its first operations.
The engine is a single native executable, written in Rust, that exposes its operations as MCP tools over stdin/stdout. Every host launches it as a child process, either for a long session or for a single call. The FPGA work itself is done by proven open source tools that the engine downloads and drives.
Figure 1. Proposed architecture
Local frontends connect to the engine directly. Remote frontends go through the optional network adapter layer.
Host connections¶
All hosts launch the engine as a child process and use MCP over stdin/stdout. Table 3 in the requirements lists the frontend deployment forms these hosts serve. They differ in session length and in how they use MCP. Session length is chosen by each host and does not affect the engine design.
Table 1. Host connections
| Host | Examples | Session | MCP usage |
|---|---|---|---|
| Desktop apps and IDE extensions | Logix IDE, Logix VSCode, IceStudio | Ongoing | Calls known tools by name |
| CLI tools, scripts and CI | Logix CLI, shell scripts, GitHub Actions | One call per command | Calls one known tool by name |
| HDL programs | Amaranth, LiteX | Ongoing | Calls known tools by name |
| AI agents | MCP-based agents | Ongoing | Discovers tools and their descriptions, then decides what to call |
| Network adapter (nice to have) | Serves browser apps and remote labs | Ongoing | Relays tool calls from remote frontends over MCP over HTTP, with auth and file transfer |
Note: All hosts use the same MCP interface and engine schema. A one-shot call is just a short session with a single tool call, so the engine has no separate command-line mode.
Components¶
Table 2. Engine components
| Component | Role |
|---|---|
| MCP interface | Accepts MCP sessions over stdin/stdout and exposes each operation as a tool |
| Protocol core | Defines the input and output schema of every tool, plus progress and cancellation. The schema is generated from the code |
| Operation engine | Runs operations such as build, simulate and program board as isolated subprocesses, using the inputs and folders given in each request |
| Package manager | Downloads, updates and verifies toolchain packages in the engine folder |
| Catalog | Board and FPGA definitions kept as versioned data files, not code |
Technology selection¶
Table 3. Proposed technologies
| Area | Choice | Reason |
|---|---|---|
| Implementation language | Rust | Chosen mainly for its WebAssembly support, so the engine can also run fully in the browser. It also gives one static binary per platform with no runtime to install, and a strict compiler that catches many AI mistakes |
| Host interface | MCP over stdin/stdout | One standard interface for all hosts, with tool discovery for AI agents and client libraries in many languages. Pinned to MCP 2026-07-28, whose stateless core matches the engine's stateless requests. Clients on MCP 2025-11-25 are also accepted as needed. MCP roots, deprecated in 2026-07-28, are not used |
| MCP library | Official Rust MCP SDK (rmcp) | Implements the MCP protocol, so the engine only defines its tools. Version 3.8 (October 2026) supports MCP 2026-07-28, older clients down to 2025-11-25, and the Tasks extension |
| Async runtime | Tokio | Runs an operation alongside its progress reporting and cancellation |
| Synthesis | Yosys | Standard open source synthesis tool |
| Place and route | nextpnr | Supports the main open FPGA families with IceStorm, Project Trellis and Apicula |
| Simulation | Icarus Verilog, Verilator | Event-driven and fast compiled simulation |
| Lint and format | Verilator, Verible | Mature open source checkers and formatter |
| Board programming | openFPGALoader | Supports a wide range of boards and programmers |
| Toolchain packages | Prebuilt per-platform builds, e.g. from OSS CAD Suite | Avoids building tools from source on user machines |
| In-browser toolchain | YoWASP | Yosys and nextpnr compiled to WebAssembly, for browser apps that run fully in the browser |
| Network adapter (nice to have) | MCP over Streamable HTTP with OAuth | The standard MCP transport and authorization for remote frontends |
Key design decisions¶
- Follows the requirements. MCP for all hosts, a tool API that never breaks, and stateless requests, as the requirements require.
- Process isolation. Each tool runs as a subprocess with its own working directory and environment. Cancelling an operation stops its process tree. Stopping asks first (SIGTERM), so a tool can finish writing its files, such as a waveform, and kills what is left after two seconds.
- Tool environment. The engine starts each tool from an empty environment with only
PATH(the core component's program folder, fromlogix-core.json, then/usr/bin:/bin),HOMEandTMPDIR, so results never depend on the user's shell setup, such as aPYTHONPATHthat would confuse the packaged Python. On macOS and Linux, oss-cad-suite'sbinprograms are launcher scripts that set each tool's own variables, such asVERILATOR_ROOTfor Verilator,PYTHONHOMEfor nextpnr and vvp,OPENFPGALOADER_SOJ_DIRfor openFPGALoader and the GTK settings for GTKWave, so the engine sets nothing else. Later platforms and packages need more: Windows has no launcher scripts, so the engine must set what the tools need there (the packages'libfolders onPATH,VERILATOR_ROOT,OPENFPGALOADER_SOJ_DIR), and the ECP5 and Xilinx flows pass Trellis and Project X-Ray database paths in their commands. Every result lists the exact commands and environment (see the tool API's Commands section). - Display paths. How paths read for people is applied in one place,
src/display_paths.rs, around every tool call (tools::call): the result's diagnostics, messages and testbench output, and the live output through a wrapping sink. Tools only use it for text they write elsewhere, such as the diagram's source places (in its model and page). After the request'sdisplay_pathsentries come the request's own paths as it gave them (custom board files as given, the source folder's files relative to it), so the default needs no special cases in tools. Paths are matched by real location: spellings (as given and resolved) and the file's identity on disk. - Output rules. What the engine knows about how particular tools print lives in one table,
src/output_rules.rs. A rule names the tools it applies to (or any tool), a pattern on the line as printed, and an action: hide the line from the live output, show it at another level, remove noise around the message (never its words), add a note from the engine after it (once per tool run), start a progress phase, or read the phase's percentage. Rules change only the live output and progress, never results or diagnostics, which come from the raw output. Hidden lines go to the engine log with the rule's ID. Each rule records why it exists and where its lines come from, with real lines it must match and nearby lines it must not, checked by a test, along with a check that it never fires for other tools. Rules are internal and can change in any version; frontends never repeat them. Which lines a tool prints over (a bare carriage return) isn't a rule: the runner reads it from the tool's output and sends it asredraw. - Concurrency. Each instance serves one session. Several instances can run at once and share an engine folder, using file locks during package downloads.
- Long operations. Every operation is a plain tool call with progress and cancellation, which is all local hosts need. MCP 2026-07-28 supports both on stdin/stdout: the host requests progress with a progress token and receives progress notifications, and cancels by sending a cancellation notification with the request ID. The MCP Tasks extension is added later for AI agents, whose tool calls may time out, and for the network adapter, where connections can drop. Inside the engine, both use the same operation model.
Engine testing¶
How the project tests the engine itself. Testing user designs is an engine feature, covered under User design testing in the requirements.
- Schema tests: every tool's inputs and outputs are checked against the published schema.
- Golden tests: the engine's own test designs, Verilog sources with a board ID kept with the engine's tests, are built for each FPGA family and their outputs compared with known-good results.
- Platform matrix: CI runs the full suite on every supported platform and architecture.
- Hardware-in-the-loop tests: check the engine's own board programming and on-board support on real boards. A lab computer on a supported platform runs them nightly, through a hardware test interface that can drive and observe the boards. The interface is still open.
Distribution¶
Per-platform binaries are published on GitHub Releases. PyPI and npm packages make the binary easy to install from Python and JavaScript projects, and a separate npm package holds the WebAssembly build. Each host bundles the engine version it needs.
Appendix: Code examples¶
These examples are illustrative. They follow the Tool API and the Command line.
Example 1: a command-line tool calling the engine's build operation (Python)¶
A command-line tool, such as Logix CLI, launches the engine, calls the build tool with explicit folders and a board ID, shows progress, and reads the structured result or error. It uses the official MCP Python SDK.
import asyncio
import os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
# Launch the engine as a child process, passing its engine folder.
ENGINE = StdioServerParameters(
command="logix-engine", # engine binary bundled with the tool
args=["--engine-folder", os.path.expanduser("~/.myhost/engine")],
)
async def on_progress(progress, total, message):
print(f"[{progress}/{total or '?'}] {message or ''}")
async def build():
async with stdio_client(ENGINE) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize() # not needed with MCP 2026-07-28
result = await session.call_tool(
"build",
{
"board": "icestick", # a board ID (catalog or custom boards folder)
"top_module": "main",
"source_folder": "/home/user/proj",
"source_files": ["main.v"],
"constraint_file": "pinout.pcf",
"build_folder": "/home/user/proj/_build",
},
progress_callback=on_progress,
)
data = result.structuredContent
if result.isError:
# Error model: category, message, diagnostics, suggested action.
print(f"{data['category']}: {data['message']}")
for d in data.get("diagnostics", []):
print(f" {d['file']}:{d['line']}: {d['text']}")
return 1
print("Bitstream:", data["bitstream"])
print(data["report"]) # the engine's text for people
return 0
asyncio.run(build())
The tool needs no knowledge of the underlying toolchain. All folders are explicit in the request, as the stateless rule requires, and errors arrive as data that the tool formats for its own command line.
Example 2: IceStudio calling the build operation (JavaScript)¶
IceStudio's designs are block diagrams, so it first turns the diagram into Verilog and a constraint file, writes them to a source folder, and then calls the engine. Design errors come back with file and line, which IceStudio maps back to the blocks that produced them. It uses the official MCP TypeScript SDK.
const fs = require("fs");
const path = require("path");
const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
// IceStudio's own pieces, not part of the engine:
// compiler.generate(design) turns the block diagram into Verilog and a
// constraint file, plus a source map from Verilog lines back to blocks.
// ui.* shows progress, errors and highlights in the IceStudio window.
const compiler = require("./compiler");
const ui = require("./ui");
async function build(design, workDir) {
// 1. Turn the block diagram into files the engine can read.
const sourceFolder = path.join(workDir, "src");
const buildFolder = path.join(workDir, "build");
fs.mkdirSync(sourceFolder, { recursive: true });
const { verilog, pcf, sourceMap } = compiler.generate(design);
fs.writeFileSync(path.join(sourceFolder, "main.v"), verilog);
fs.writeFileSync(path.join(sourceFolder, "main.pcf"), pcf);
// 2. Launch the engine bundled with IceStudio.
const transport = new StdioClientTransport({
command: "logix-engine",
args: ["--engine-folder", path.join(ui.homeDir(), ".icestudio", "engine")],
});
const client = new Client({ name: "icestudio", version: "1.0.0" });
await client.connect(transport);
try {
// 3. Build, with progress in IceStudio's status bar.
const result = await client.callTool(
{
name: "build",
arguments: {
board: design.board, // a board ID (catalog or custom boards folder)
top_module: "main",
source_folder: sourceFolder,
source_files: ["main.v"],
constraint_file: "main.pcf",
build_folder: buildFolder,
},
},
undefined,
{ onprogress: (p) => ui.showProgress(p.progress, p.total, p.message) },
);
// 4. Show the result on the diagram.
const data = result.structuredContent;
if (result.isError) {
for (const d of data.diagnostics ?? []) {
const block = sourceMap.blockAt(d.line); // Verilog line -> block
ui.highlightBlock(block, d.text);
}
ui.showError(`${data.category}: ${data.message}`);
return;
}
ui.showResources(data.resources); // name, description, used, available, percent
ui.showSuccess("Build done");
} finally {
await client.close();
}
}
module.exports = { build };
The engine never sees the block diagram. IceStudio owns the design format and the mapping back to blocks, while the engine does the build from plain files in shared folders.
Example 3: a VS Code extension calling the build operation (JavaScript)¶
The extension runs a build with a cancellable VS Code progress notification, and shows design errors in the Problems panel. It uses the official MCP TypeScript SDK, which also works from JavaScript.
const vscode = require("vscode");
const os = require("os");
const path = require("path");
const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
const diagnostics = vscode.languages.createDiagnosticCollection("logix");
async function build(projectDir) {
// Launch the engine as a child process, passing its engine folder.
const transport = new StdioClientTransport({
command: "logix-engine", // engine binary bundled with the extension
args: ["--engine-folder", path.join(os.homedir(), ".logix", "engine")],
});
const client = new Client({ name: "logix-vscode", version: "1.0.0" });
await client.connect(transport);
try {
await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification, title: "Logix VSCode build", cancellable: true },
async (progress, token) => {
// Cancelling in VS Code cancels the engine operation.
const abort = new AbortController();
token.onCancellationRequested(() => abort.abort());
const result = await client.callTool(
{
name: "build",
arguments: {
board: "icestick", // a board ID (catalog or custom boards folder)
top_module: "main",
source_folder: projectDir,
source_files: ["main.v"],
constraint_file: "pinout.pcf",
build_folder: `${projectDir}/_build`,
},
},
undefined,
{
signal: abort.signal,
onprogress: (p) => progress.report({ message: p.message }),
},
);
const data = result.structuredContent;
diagnostics.clear();
if (result.isError) {
// Show design errors in the Problems panel, at their file and line.
for (const d of data.diagnostics ?? []) {
const uri = vscode.Uri.file(`${projectDir}/${d.file}`);
const line = Math.max(d.line - 1, 0);
const range = new vscode.Range(line, 0, line, 0);
diagnostics.set(uri, [
...(diagnostics.get(uri) ?? []),
new vscode.Diagnostic(range, d.text, vscode.DiagnosticSeverity.Error),
]);
}
vscode.window.showErrorMessage(`${data.category}: ${data.message}`);
return;
}
vscode.window.showInformationMessage(`Build done. ${data.report_summary}`);
},
);
} finally {
await client.close(); // closes the session; the engine exits
}
}
module.exports = { build };
The extension maps engine results onto VS Code's own features: progress and cancellation onto a progress notification, and diagnostics onto the Problems panel. It never parses tool output.