Skip to main content
Glama

WEFT — WEFT Elaborates FPGA Toolchains

ci container docs python latest tag licence: GPL-3.0-only Contributor Covenant 2.1

An MCP server that gives an LLM client a safe, structured interface to an Intel Quartus Prime 25.1 FPGA flow: lint and simulate in seconds, compile asynchronously, and read results back as JSON instead of megabytes of log.

The name is a GNU-style recursive acronym. The weft is the thread woven across the warp to make fabric, and routing logic into FPGA fabric is precisely the job.

Status

WEFT is under construction, milestone by milestone. What is finished today:

Tool

State

lint

working — Verilator for Verilog and SystemVerilog, GHDL for VHDL

simulate

working — Verilator, Icarus or GHDL in the container, host Questa for mixed-language designs, with waveform capture

Quartus projects

working — create_project, set_assignments, get_project_info, list_projects

Quartus compilation

working — start_compile as a persistent job, get_job_status, get_job_log, cancel_job

parse_reports

working — resources, timing per clock, ranked messages

Source indexing

working — index_project, get_module_info, get_hierarchy, search_code

Document RAG with OCR

working — index_document, search_docs, list_indexed_docs; clause-level citations

Documentation generation

working — generate_docs, generate_module_doc; Markdown and HTML, auto-indexed

Device programming

not yet — and see What has not been tested on hardware below

Both transports work: stdio for a local client, Streamable HTTP behind a static bearer token for a client on the LAN.

Related MCP server: fpgaZeroMCP

What this is for, if MCP is new to you

Say a testbench fails and you want a model's help with it. Today you copy the file into a chat window, run Verilator yourself, paste a screenful of %Warning-WIDTHEXPAND after it, read the answer, apply the fix by hand, and go round again. The design is three files deep, so either you paste all three or the model guesses at the two you left out — and it will guess, confidently. The answer you get is about the text you pasted, which is not necessarily what is on disk.

MCP, the Model Context Protocol, removes the ferrying. A server advertises a list of tools and the arguments each one takes. An LLM client — Claude Desktop, Claude Code, or anything else that speaks the protocol — puts that list in front of the model. You keep typing prose. The model picks a tool, fills in the arguments, and the client sends the call. WEFT is the server on the far end. It holds no model, runs no inference, and makes no network calls at runtime.

When the model calls lint, WEFT resolves every path against your workspace root, refuses anything that escapes it, and runs roughly this:

podman run --rm --network=none -v <workspace>:/work -w /work weft-tools \
    verilator --lint-only -Isrc src/updown_counter.sv

Verilator prints what it always prints. WEFT turns that into records — file, line, severity, message — and the container is gone. simulate is the same loop around Verilator, Icarus or GHDL, handing back pass/fail, a tail of the log and the path to the waveform.

The boundary matters more than the plumbing: the model chooses what to attempt, WEFT chooses what may execute. There is no shell on the far end. The model cannot invent a flag, cannot reach a path you have not opened to it, and cannot run anything that is not on the list.

The other reason to wrap the tools is size. A Quartus compile leaves megabytes of .rpt behind, and what you wanted from it was a resource line, an Fmax per clock domain, and the two warnings that mattered. Tool results here are a few kilobytes of JSON; the raw logs stay on disk and are fetched by name when something actually needs them.

None of this designs anything. It will not write your RTL, close your timing, or know which board is on your desk. It runs the commands you would have run, and hands back something small enough to reason about.

How it is put together

Quartus runs on the host — WEFT drives the installation you already have and never tries to install or containerise it. Everything else WEFT executes lives in one Podman image, weft-tools: Verilator, Icarus Verilog, GHDL and Verible for HDL work, Tesseract and Poppler for reading documents. The container runs with --network=none and sees nothing but your workspace.

Every path an MCP client supplies is resolved and checked against the configured workspace root before it reaches the filesystem, on the host and inside the container alike.

Nothing reaches the network at runtime, and nothing reports telemetry.

The full design — every tool's arguments and return shape, the milestones, and the reasoning behind the awkward parts — is in PROJECT.md.

The manual is at weft-mcp.readthedocs.io, built with Sphinx from Documentation/. Its tool reference and configuration reference are generated from the code on every build, so neither can drift from what the server actually accepts — a key added without a description fails the build. Build it yourself with pip install -e '.[docs]' && sphinx-build -W Documentation _build.

Requirements

  • Quartus Prime 25.1 (Lite, Standard or Pro) installed and licensed by you

  • Questa - Altera Starter FPGA Edition, optional; it ships beside Quartus and is the only way to simulate a design that mixes Verilog and VHDL in one run

  • Podman, rootless

  • Python 3.11 or newer

  • jtagd for programming hardware, once that milestone lands

Quick start

Arch Linux

sudo pacman -S --needed podman python git

git clone https://github.com/FPGArtktic/weft-mcp.git
cd weft-mcp
./setup.sh

setup.sh builds the image, installs the package, finds your Quartus and Questa, writes a starter configuration and prints the command to register the server. ./setup.sh --check reports what it would find and changes nothing. The manual equivalent is:

podman build -t weft-tools -f containers/Containerfile.weft-tools .
pip install --user .

Ubuntu 24.04 LTS

sudo apt update
sudo apt install podman uidmap python3 python3-pip git

git clone https://github.com/FPGArtktic/weft-mcp.git
cd weft-mcp
./setup.sh

uidmap is only a Recommends of podman, so a plain apt install pulls it in but --no-install-recommends does not. Rootless Podman needs it.

Ubuntu 22.04 ships Python 3.10, which is below what WEFT needs. Either move to 24.04 or install a newer interpreter, for instance with uv:

uv venv --python 3.12 && uv pip install .

Building the image

The image is never distributed — you build it, which keeps WEFT's own distribution to GPL-3.0-only code and avoids shipping an aggregate of third-party binaries under mixed licences. podman build is the only step that needs network access; everything afterwards runs offline.

GHDL is compiled from source during the build, so expect it to take a while the first time.

Configuring

WEFT reads one TOML file, found at --config, then $WEFT_CONFIG, then ~/.config/weft/weft.toml. Only [workspace] is required. Every key is documented in the configuration reference, generated from the loader itself.

[workspace]
# Nothing outside this directory can be read or written.
root = "/home/you/fpga"

[container]
image = "weft-tools"

[quartus]
edition = "lite"          # omit when only one edition is configured

[quartus.lite]
root = "/home/you/intelFPGA_lite/25.1std/quartus"

[quartus.pro]
root = "/opt/intelFPGA_pro/25.1/quartus"
# FlexLM variables are passed through to every Pro invocation.
env = { LM_LICENSE_FILE = "1800@licence-server" }

[questa]
# Ships beside Quartus. The only simulator here that reads Verilog,
# SystemVerilog and VHDL in one run. Never a default — ask for it by name.
root = "/home/you/altera_lite/25.1std/questa_fse"
env = { SALT_LICENSE_SERVER = ";/home/you/.altera.quartus/questa_lic.dat" }

[jobs]
timeout_s = 7200

[rag]
# Where your PDFs live. Mounted read-only, and separate from the workspace
# because a document collection outlives any one project. Defaults to the
# workspace when omitted.
library = "/home/you/Documents/fpga-docs"

# Local BGE-M3 weights in ONNX form. Omit the key and document search still
# works, by text rather than by meaning.
model_path = "/home/you/.local/share/weft/models/bge-m3"

# The index itself. Defaults to <workspace>/.weft/documents.sqlite; put it
# somewhere shared to index a library once and use it from every project.
database = "/home/you/.local/share/weft/documents.sqlite"

[http]
host = "127.0.0.1"
port = 8080
token = "put-a-long-random-string-here"

Quartus paths always come from here. WEFT never guesses them and never searches PATH. A machine with no Quartus simply omits the section — lint and simulate do not need it.

Unknown keys are refused rather than ignored, so a typo fails at startup instead of silently doing nothing.

Running

The fastest way in, if you cloned the repository:

./setup.sh

It checks what is on the machine, builds the container image, installs the package, writes a starter weft.toml with whatever Quartus and Questa it found, and prints the exact command to register the server. It never installs Quartus — that is yours to install and licence — and ./setup.sh --check reports what is present without changing anything.

Or by hand:

weft --transport stdio        # a local client
weft --transport http         # on the LAN, behind a bearer token

Registering it with a client

Running weft in a terminal on its own does nothing useful. It speaks MCP over its standard input, so a client has to start it. Where you put that instruction depends on the client.

Claude Code, the CLI. One command, no file to edit:

claude mcp add --scope user weft -- \
    ~/.local/bin/weft --transport stdio --config ~/.config/weft/weft.toml

The -- matters: everything after it is the command Claude Code runs, not an argument to claude mcp add. --scope user makes the server available in every project; --scope project writes a .mcp.json in the current directory instead, which is what you commit so your colleagues get the same server. Then:

claude mcp list        # is it there, and did it connect
claude mcp get weft    # why did it not

Inside a session, /mcp shows the same thing.

Claude Desktop. Edit its configuration file — the app does not have a command for this:

Linux

~/.config/Claude/claude_desktop_config.json

macOS

~/Library/Application Support/Claude/claude_desktop_config.json

Windows

%APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "weft": {
      "command": "/home/you/.local/bin/weft",
      "args": ["--transport", "stdio", "--config", "/home/you/.config/weft/weft.toml"]
    }
  }
}

Use the absolute path to the binary. Claude Desktop does not inherit your shell's PATH, so "command": "weft" works from your terminal and fails from the app, which is the single most common reason a server never appears. Restart the app after editing; it reads the file at startup and not again.

Over HTTP, once the server is running somewhere with a token configured:

claude mcp add --transport http --header "Authorization: Bearer YOUR_TOKEN" \
    weft https://your-host.example.com/mcp

Every request must carry that header; anything else gets a 401. Set a real token in the configuration — the HTTP transport refuses to start without one.

When the server does not appear

In order of how often it is actually the cause:

  1. The path is not absolute, and the client's PATH does not have ~/.local/bin in it. Run command -v weft and paste what it prints.

  2. You added it in the wrong scope. claude mcp add without --scope registers it for the current directory only. claude mcp list from another project will not show it.

  3. The configuration is wrong, and WEFT said so and exited. Run the same command by hand — weft --transport stdio --config ... — and read the error. A misspelled key is refused by name, not ignored.

  4. The JSON is malformed. Claude Desktop fails silently on this. Its logs are in ~/Library/Logs/Claude/ on macOS and %APPDATA%\Claude\logs on Windows.

Why there is an HTTP transport at all

A local client does not need one; stdio is simpler and has no token to leak. HTTP exists because it is the hand-off point for running this behind a model you host yourself, on a network with no way out. Such a model, served behind an OpenAI-compatible endpoint, talks to the same /mcp endpoint, and nothing on the server side changes. WEFT already makes no network calls at runtime, so an installed server needs nothing further.

Building that deployment — the inference cluster, the serving stack, carrying the image and the wheels across the gap — is not part of this repository. Appendix A of PROJECT.md records what it would take and stops there, deliberately.

Your first session, if this is all new

You do not type tool calls. This trips up everybody once, so let us get it out of the way: you talk to the model in ordinary sentences, the model decides which of WEFT's tools to call and with what arguments, and the client sends the call. The JSON in this README is what goes over the wire. You never write it.

What follows is a real first session against the project bundled in this repository. It needs no FPGA board and, for the first half, no Quartus either.

1. Point the workspace at something

[workspace]
root = "/home/you/weft-mcp/examples/counter"

[container]
image = "weft-tools"

That directory is the entire world as far as WEFT is concerned. A path that resolves outside it is refused — symlinks resolved first, so the obvious trick does not work either. If you get "path escapes the workspace", that is not a bug, that is the sandbox doing the one job it has.

2. Ask for something

Start your client and type:

Lint the counter design.

The model calls lint with the source list. On this project Verilator answers:

error MODMISSING: Cannot find file containing module: 'seven_seg_decoder'
excluded: ["src/seven_seg_decoder.vhd"]

Read that carefully, because it teaches you more than a clean run would. The module is not missing. It is written in VHDL, Verilator does not read VHDL, and WEFT told you exactly which file it had to leave out instead of pretending the question was answered. If you have Questa configured, say lint it with Questa and the whole thing checks clean, both languages at once.

3. Run a testbench

Run the up/down counter testbench.

passed: true   simulator: verilator
[155000] PASS: counts down when up is low
[165000] PASS: clear beats enable
[175000] PASS: wraps downwards
=== TEST PASSED ===
waveform: .weft/waves/updown_counter_tb.vcd

The waveform is a real file on your disk. Open it in GTKWave. WEFT does not pretend to show it to you.

Now try the top-level one:

Run the counter_top testbench.

passed: false
excluded: ["src/seven_seg_decoder.vhd"]
%Error: ... /tmp/weft-build/seven_seg_decoder.sv

It fails, and it fails honestly. Verilator was handed a design whose display decoder is VHDL, could not read it, said which file it dropped, and then tripped over the module that was consequently missing. Ask for it with Questa and it passes, all three languages elaborated together, excluded empty. That is the one thing a proprietary simulator buys you here and the reason it is wired in at all.

4. Ask about the design

What does the debouncer's rise_pulse port do?

The model calls index_project once, then get_module_info. The answer — "one cycle high on every 0 -> 1 edge of clean_out" — is not the model's guess. It is the line the author wrote in the header comment above the module, parsed out of the source. That is the whole point of the exercise: you can tell the difference between what was read and what was invented, because WEFT only returns the former.

5. Compile it (this part needs Quartus)

Compile the counter project.

Compilation does not block. You get a job id back immediately, because a Quartus compile takes minutes and a tool call that sits there for four of them is a tool call that times out. Ask how is it going and the model calls get_job_status. Kill the server mid-compile and restart it — the job is still there, and its final status is still correct. That was not free to build, and it is the difference between a toy and something you can leave running.

When it finishes:

Summarise the compilation.

Status: Successful. Timing met on 1 of 1 clock. 2 critical warnings to look at.
Device            10M04SAE144A7G
Fullest resource  20 %
Worst slack       0.144 ns (Hold, clk)
Lowest Fmax       154.23 MHz (clk)

Every one of those numbers came out of a .rpt file. None was rounded, guessed or summarised by a language model on the way.

Four things that will bite you

Nothing happens automatically. No watcher, no background indexing, no scan at startup. index_project reads a directory when you ask. index_document reads one PDF when you ask. If a search comes back empty, the usual reason is that you never indexed the thing. Ask what documents are indexed — the answer also lists what is sitting in your library that is not.

Paths are workspace-relative, always. src/counter_top.sv, not /home/you/.... Both work, the second only because it is checked and rewritten, and if it lands outside the root it is refused.

The model is still a model. WEFT does not make it right. It makes it checkable: every claim it repeats back to you came from a tool, and you can run the same tool yourself and get the same answer. When it tells you your timing closed, that came from the timing analyser. When it tells you your design is elegant, that came from nowhere.

Read the excluded and the retrieval fields. They are there because a result that quietly answered a narrower question than you asked is worse than no result. excluded names sources a tool could not read. retrieval says whether a document search was semantic or a substring match — those rank differently, and a ranking you misread is a citation you will misuse.

When it goes wrong

Nothing here needs the network at runtime, so a hang is local. In order of how often it is actually the cause:

  • The container is not built. podman build -t weft-tools -f containers/Containerfile.weft-tools .

  • The workspace root does not exist, or the path you gave is outside it.

  • Quartus is not configured. Lint and simulate do not need it. Everything with compile or project in the name does, and WEFT will not go looking for it on PATH — the configuration says where it is or it does not run.

  • A key in the config is misspelled. WEFT refuses unknown keys at startup rather than ignoring them, so you get told, with the key name.

Raw logs stay on disk. get_job_log fetches the tail of one by name. If you want the whole thing, it is in output_files/ where Quartus left it.

Simulating a mixed-language design

Verilator, Icarus and GHDL each read one language. A design written in more than one can therefore only be simulated a module at a time, with the other language's blocks left out — which is exactly the part a hierarchy test exists to exercise. simulate says so honestly: the sources it dropped come back in excluded.

Questa reads all three at once. If the configuration names it, ask for it:

simulate {
  "files": ["src/counter_top.sv", "src/debouncer.sv", "src/updown_counter.sv",
            "src/clk_tick.v", "src/seven_seg_decoder.vhd"],
  "top": "counter_top_tb",
  "testbench": "tb/counter_top_tb.sv",
  "simulator": "questa"
}

excluded comes back empty: the VHDL decoder is elaborated inside the SystemVerilog top, and the whole hierarchy runs. Questa is proprietary and licensed, so it runs on the host like Quartus does and is never picked by default — you name it.

One thing worth knowing, since it decides whether the result means anything: vsim -c exits 0 after a $fatal, so a run that stopped on a failed assertion looks exactly like one that passed. WEFT reads Questa's own TESTSTATUS after the run instead, and reports a warning as a pass, an error or a fatal as a failure.

Reading your own documents

Your PDFs go in the directory [rag] library points at — not in the workspace. Standards and vendor handbooks are a personal library that outlives any one project, and forcing a copy into every project tree would be absurd. That directory is mounted read-only, so indexing cannot write anything into your collection, and the index lands wherever [rag] database says. Point the database at a shared path and one library serves every project.

Nothing is indexed on its own. There is no watcher, no scan at startup, no background pass over the library — exactly as for source code, which index_project reads only when asked. index_document reads one document when you name it, and never before. list_indexed_docs shows what is indexed and what is sitting in the library un-indexed, because otherwise a model has no way to ask for a document whose filename it has never seen.

index_document takes a PDF from the library and makes it searchable. Pages with a text layer are extracted directly; only pages that come back empty — a scan — are rendered and passed to Tesseract, so a born-digital 1300-page standard is indexed in seconds rather than an hour of pointless OCR.

The document is cut at its own headings, not into fixed-size windows, so a result cites 1800-2017 §9.2.2.4 and you can look that up. A window number could not be checked against anything.

Search is semantic when an embedding model is configured and textual when it is not, and the result says which one answered — a ranking that came from a substring match must not be mistaken for one that came from meaning. The difference is real: against the SystemVerilog standard, "weighted random selection" finds §18.16 randcase by meaning and finds nothing at all by text, because those three words do not appear in it.

No model ships with WEFT. BGE-M3 is MIT-licensed and about 2 GB; you fetch it once, point model_path at it, and nothing touches the network again:

DIR=~/.local/share/weft/models/bge-m3
mkdir -p $DIR/onnx
for f in onnx/model.onnx onnx/model.onnx_data tokenizer.json; do
    curl -Lo $DIR/$f https://huggingface.co/BAAI/bge-m3/resolve/main/$f
done

Point model_path at $DIR. The graph is small; model.onnx_data beside it holds the weights and is the 2 GB.

Documents are yours and stay yours. WEFT ships no standards, no handbooks and no vendor documentation, and the index it builds never leaves your workspace.

Generated documentation

generate_docs writes a reference for a project and generate_module_doc for a single module. Everything in them was computed by something: port and parameter tables from the Verible and GHDL syntax trees, per-port descriptions from the kernel-doc headers in your sources, the pin map from the fitter's own .pin report, the resource and timing figures from the compilation reports.

What it does not do is describe what your module is for. A generator that invents a sentence about a port produces a document that reads like a reference and is not one, and you cannot tell which lines were computed and which were guessed. An undocumented port gets a dash. Prose is the client model's job — it can read the same facts and write them up, and then you know who said what.

examples/counter/docs/counter.md is committed output, not a mock-up: 177 logic elements and 154.23 MHz from a real compilation, and GitHub draws the hierarchy diagram.

Markdown gets the hierarchy as Mermaid, which GitHub and most Markdown viewers render. HTML gets the same tree as inline SVG instead: a browser draws no Mermaid, and a server that makes no network calls cannot hand it a renderer, so an HTML page carrying Mermaid source would show the source. Generated documents are indexed for retrieval as they are written, under doc_type: "generated".

The demo project

examples/counter/ is a small MAX 10 counter written in SystemVerilog, Verilog-2001 and VHDL at once. The three languages are the point: no open-source simulator reads more than one, so the project is a fair test of whether a tool really handles a mixed hierarchy or only claims to.

What has not been tested on hardware

I have no FPGA board. Everything above was developed and verified against a real Quartus Prime 25.1 Lite installation and a real Questa - Altera Starter FPGA Edition, on real sources — the demonstration project compiles, fits, reports 177 logic elements and 154.23 MHz, and its testbenches run — but the bitstream has never been loaded into a device, because there is no device here to load it into.

Concretely, that means:

  • program_device and list_devices are unverified against hardware. When they land, what I can check is that WEFT reaches quartus_pgm and jtagconfig with the right arguments and reports what they say, including reporting an empty JTAG chain as empty rather than as an error. Whether a board on the far end actually accepts the bitstream, I cannot tell you.

  • The .sof and .pof a compilation of the demo project produces were written by the assembler and never programmed. They are evidence that the flow completed, not that the design works on silicon. They are build output and are not committed.

  • Timing closure is reported, not proven. WEFT reads back what the timing analyser computed. Nothing here has been correlated with a scope.

The parts that touch a board are also the parts that break in ways no unit test finds — a cable that enumerates differently, a JTAG chain with something unexpected on it, a device the .pof does not suit. If you run this against hardware and it misbehaves, that is a bug report I want, and I will not be able to reproduce it.

Everything else — lint, simulate, compile, parse, index, retrieve, document — was run against the actual tools, not mocked, before it was called working.

What CI checks

Every push runs three jobs: ruff for style, pytest on Python 3.11, 3.12 and 3.13, and a Sphinx build with warnings as errors — which is what makes the generated tool and configuration references worth anything, since a key added without a description fails the build rather than shipping a reference that is quietly incomplete.

The eighteen tests that need the weft-tools image are not in that run — the second badge above is theirs. They skip themselves when the image is absent, and building it compiles GHDL from source, which measured at eight minutes on a runner. A separate container workflow builds it for real and runs them where a break would mean something: when the Containerfile or the code driving it changes, weekly because Arch is a rolling release, and on demand. It also asserts that the tests actually ran — a run where they all skipped would otherwise pass having tested nothing.

Contributing

Patches are welcome. WEFT follows the Linux kernel's habits: one logical change per commit, subsystem: summary subjects, a body that explains why, rebase rather than merge, and a Signed-off-by: line on everything. See CONTRIBUTING.md.

Author

WEFT is written and maintained by Mateusz Okulanisfpgartktic.github.io, @FPGArtktic, FPGArtktic@outlook.com.

Bug reports, patches and disagreements are all welcome — the last of those especially, if you have driven this toolchain harder than I have.

Licence

Copyright (C) 2026 Mateusz Okulanis.

GPL-3.0-only. The full text is in COPYING.

WEFT invokes Quartus and the containerised tools as separate programs and distributes none of them.

Trademarks

Intel, Altera and Quartus are trademarks of their respective owners. This project is not affiliated with, endorsed by, or sponsored by Intel or Altera. It contains no Intel or Altera code, files or documentation, and it neither installs nor redistributes their software.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    C
    quality
    D
    maintenance
    Provides programmatic access to Arcas OnlineEDA platform for electronic design automation, enabling formal verification, equivalence checking, power analysis, security verification, and FPGA design through natural language and automated workflows.
    5
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Provides AI assistants with a complete FPGA toolchain for HDL linting, simulation, synthesis, and place-and-route across various hardware targets. It features a GitHub-backed IP core registry that enables users to search for and import MIT-licensed cores directly through their chat interface.
    15
    1
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Wraps Quartus II 9.1 command-line tools into MCP tools, enabling AI agents to create projects, assign pins, generate simulation waveforms, run simulations, compile, read reports, and program devices.
    16
    4
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants to drive Xilinx Vivado, Intel Quartus, and Anlogic TangDynasty for FPGA development, including project creation, synthesis, implementation, timing closure, and hardware programming through natural language.
    MIT