fontforge-mcp
Exposes FontForge as MCP tools for designing, repairing, and building fonts, with PNG previews to visually verify glyphs and compiled fonts.
README
fontforge-mcp
An MCP server that exposes FontForge as tools, so an AI assistant can design, repair and build fonts — and see the result, because every operation can be rendered back as a PNG.
Draw glyphs from SVG paths, import an SVG folder as an icon font, clean up contours, set metrics and kerning, build OpenType features, derive weights, and compile variable fonts. Then look at a proof sheet before claiming it works.
Requirements
| FontForge | 2023-01-01 or newer | tested on 20251009 |
| Python | 3.10+ | for the server; mcp requires 3.10 |
mcp, Pillow, fonttools[woff] |
see requirements.txt |
installed into a venv |
| An MCP client | Claude Code, or any other | stdio transport |
That's the whole list. No Node, no compiler, no Rust toolchain.
You do not install the Python packages into FontForge. They go into a normal Python venv that talks to FontForge over a pipe. The next section explains why — it is the single most important thing to understand before you file a bug.
Why two processes
The obvious design is one process: run the MCP server inside FontForge's own
Python, since that's where import fontforge works. On Windows that is
impossible, for two measured reasons:
$ ffpython -c "import sysconfig; print(sysconfig.get_platform())"
mingw_x86_64_msvcrt_gnu
$ ffpython -m pip install pydantic
ModuleNotFoundError: No module named 'pip._vendor.distlib'
FontForge for Windows bundles a MinGW-built CPython. PyPI's win_amd64
wheels are MSVC-ABI and will not load in it, and the bundled pip is broken out
of the box. So mcp and its pydantic dependency can never live there.
Hence the split:
MCP client (Claude Code, …)
│ MCP over stdio
▼
server.py your Python 3.10+ venv
├─ mcp tool definitions and schemas
├─ Pillow preview rendering via FreeType
└─ fontTools binary inspection, WOFF2, variable fonts
│ newline-delimited JSON over pipes
▼
worker.py FontForge's interpreter — stdlib only
└─ holds open fonts in memory between calls
On Linux and macOS the fontforge module is usually importable from a system
python3, so the "two interpreters" may resolve to the same binary. The
architecture is unchanged; only the discovery result differs.
One more detail worth knowing: libfontforge writes notices straight to fd 1
from C, which would corrupt the JSON stream. The worker's first act is to dup the
real stdout to a private descriptor and point fd 1 at stderr.
Install
1. FontForge
Windows — download the installer from github.com/fontforge/fontforge/releases. It is Inno Setup, so it takes silent flags:
.\FontForge-2025-10-09-Windows-x64.exe /VERYSILENT /NORESTART
ffpython.exe lands in <install>\bin\ and is found automatically.
Debian / Ubuntu
sudo apt install fontforge python3-fontforge
Fedora
sudo dnf install fontforge python3-fontforge
macOS
brew install fontforge
On Linux and macOS you can confirm it immediately — if this prints a version, the hard part is done:
python3 -c "import fontforge; print(fontforge.version())"
On Windows there is nothing to check by hand yet: FontForge ships its own interpreter and its location depends on where you installed it. Step 3 finds it for you, on any drive — don't go hunting for the path.
2. The server
Linux / macOS
git clone https://github.com/LHxis/fontforge-mcp
cd fontforge-mcp
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
Windows — identical in cmd.exe and PowerShell:
git clone https://github.com/LHxis/fontforge-mcp
cd fontforge-mcp
python -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
3. Verify, before registering anything
Two scripts, in this order. The first finds FontForge; the second proves the whole chain works.
.venv/bin/python bridge.py # Linux / macOS
.venv/bin/python selftest.py
.venv\Scripts\python.exe bridge.py :: Windows — same in cmd.exe and PowerShell
.venv\Scripts\python.exe selftest.py
bridge.py prints every interpreter it tried, marks which ones can actually
import fontforge, and names the winner. If it finds nothing, this is where you
learn that — with the exact command to point FFPYTHON at your install. Run it
first: everything downstream depends on this one answer.
selftest.py runs 13 checks: it builds a small font from SVG paths,
validates the contours, generates TTF/OTF/WOFF2, renders a preview, and drives
the server over real MCP stdio — the same path your client will use. It also
confirms that a tool error surfaces as an error instead of killing the
connection.
If both pass, the problem is never the install. That matters later: when
something misbehaves, re-running selftest.py tells you in seconds whether to
suspect your setup or your font.
4. Register it
The client stores the command verbatim and does not expand ~ or resolve
paths relative to your shell — it needs absolute paths. You do not have to type
them: selftest.py printed the exact command for your machine at the end of
step 3. Copy that line as printed and you are done.
To build it yourself instead, let the shell fill in the path. Run these from inside the project directory — no username, no drive letter, nothing to edit:
cmd.exe
claude mcp add fontforge --scope user -- "%CD%\.venv\Scripts\python.exe" "%CD%\server.py"
PowerShell
claude mcp add fontforge --scope user -- "$PWD\.venv\Scripts\python.exe" "$PWD\server.py"
Linux / macOS
claude mcp add fontforge --scope user -- "$PWD/.venv/bin/python" "$PWD/server.py"
The shell expands %CD% / $PWD before claude ever sees it, so what gets
stored is a plain absolute path.
Keep the Windows command on one line. PowerShell's backtick and bash's backslash continuations both fail in
cmd.exe, which is still the default shell for a lot of Windows users — and a broken continuation is a confusing error to hit at the last step.
Any other MCP client — the equivalent stdio entry:
{
"mcpServers": {
"fontforge": {
"command": "/abs/path/fontforge-mcp/.venv/bin/python",
"args": ["/abs/path/fontforge-mcp/server.py"]
}
}
}
Restart the client — MCP servers are loaded at startup. Then ask it to run
ff_status; it reports the FontForge version, the interpreter in use and the
workspace path.
Configuration
| Variable | Default | Purpose |
|---|---|---|
FONTFORGE_MCP_WORKSPACE |
~/fonts |
where relative paths resolve |
FFPYTHON |
auto-detected | pin the FontForge interpreter |
How the interpreter is found
Cheapest lookup first, stopping at the first one that can actually
import fontforge — existence on disk is never enough:
$FFPYTHON— an explicit override always wins, even when it is wrong, so a bad value fails loudly instead of silently falling through.- Windows registry — FontForge's installer records
InstallLocationunder the uninstall keys. One lookup finds it on any drive, no scanning. PATH—ffpython,fontforge-python, andpython3(not on Windows, wherepython3resolves to the Microsoft Store stub and pops the Store UI).- Standard roots on every mounted drive —
%ProgramFiles%,Program Files,Program Files (x86),Programs,Apps, and each drive root from A: to Z:. This is the fallback for installs the registry does not know about. - The running interpreter — a venv built with
--system-site-packagesmay already see the module.
To see exactly what discovery finds on your machine:
.venv/bin/python bridge.py # Linux / macOS
.venv\Scripts\python.exe bridge.py # Windows
It prints every candidate it tried, which ones can import fontforge, and the
command to pin the winner.
Pinning it
Skip discovery entirely by setting the variable once:
setx FFPYTHON "D:\wherever\FontForgeBuilds\bin\ffpython.exe"
setx writes it permanently for your user, but only affects new terminals —
the one you typed it in keeps the old environment. On Linux/macOS, add
export FFPYTHON=/path/to/python3 to your shell rc.
If your MCP client is already running, restart it so it picks up the new environment.
Tools
Font · ff_status ff_new_font ff_open_font ff_list_fonts
ff_close_font ff_font_info ff_save ff_generate
Glyphs · ff_list_glyphs ff_glyph_info ff_draw_glyph ff_read_glyph
ff_delete_glyph ff_import_svg ff_import_svg_folder ff_transform_glyph
ff_clean_glyph ff_stroke_glyph
Metrics · ff_set_metrics ff_auto_metrics ff_kern
Composites & OpenType · ff_add_reference ff_build_accented ff_features
Derivation & variable fonts · ff_derive ff_build_variable_font
Verification · ff_validate ff_inspect_binary ff_preview
Escape hatch · ff_python — runs FontForge Python in the worker, with
font pre-bound. FontForge's API is far larger than this server.
ff_preview is the one that matters most. Four modes: outline (one glyph with
control points, handles and metric guides), text (real FreeType rasterization
of the compiled font), waterfall (many sizes, for small-size legibility), and
sheet (a grid of every glyph). Images come back as PNG, so the assistant can
judge the result instead of assuming it.
Tool descriptions are currently written in Brazilian Portuguese. They are instructions for the model, not UI text, and models handle them fine — but if you want them in English, they all live in the docstrings in
server.py.
Coordinate conventions
Getting these wrong is the number one cause of mangled glyphs.
- Font units, Y axis UP, baseline at
y=0. This is not screen SVG. - At
em=1000: ascender 800, descender 200, cap height ~700, x-height ~500. - Outer contours run CLOCKWISE, counters run counter-clockwise
(PostScript/cubic convention). Starting from the bottom-left corner, clockwise
means going up first.
ff_clean_glyphrepairs the direction if you get it backwards. - Round letters (
O,o,e,s) need overshoot — about 1.5% of the em past the baseline and cap height, or they read smaller than the flat letters beside them.
ff_draw_glyph accepts full SVG path syntax (M L H V C S Q T A Z plus the
relative lowercase forms) but interprets the numbers as font units.
Workflows
A typeface from scratch
ff_new_font → ff_draw_glyph (per glyph) → ff_preview mode=outline
→ ff_clean_glyph → ff_set_metrics / ff_kern → ff_preview mode=text
→ ff_generate
Draw H O n o first. Those four fix cap height, x-height, stem weight and
overshoot; the rest of the alphabet inherits all of it.
Derive from an existing font
ff_open_font → ff_font_info (rename the family!) → ff_derive → ff_generate
Icon font
ff_new_font → ff_import_svg_folder (maps from U+E000)
→ ff_clean_glyph → ff_generate .woff2
Keep the name→codepoint map the tool returns — that is what your CSS uses.
Variable font
ff_open_font(copy=true) — once per master
→ a different ff_derive on each handle → ff_generate one TTF per master
→ ff_build_variable_font
copy=true is not optional here. FontForge returns the same font object
for a file that is already open, so without it every handle points at one font:
the derives stack on each other and all your masters come out identical — with
no error to tell you.
Masters must also be interpolable: identical point counts and contour order
in every glyph. Deriving them all from one .sfd preserves that; drawing each
master by hand almost never does.
Round-tripping with the GUI
ff_save writes .sfd, FontForge's editable source format. Open it in the GUI,
adjust by hand, save, and reopen with ff_open_font. Always keep the .sfd
next to the binary — it is the font's source code; the .ttf is the build
output.
Troubleshooting
"Nenhum Python capaz de 'import fontforge' foi encontrado" — FontForge isn't
installed, or its Python bindings are a separate package (python3-fontforge on
Debian/Fedora). Point FFPYTHON at the right interpreter if it lives somewhere
unusual.
Deleting .venv fails with "access denied" (Windows) — unregistering the
server does not stop it. The process started when your client launched is still
alive and holds python.exe and the loaded .pyd files open, and Windows will
not delete a binary that is mapped into a running process. Restart the client
first, or kill the tree by hand. Note there are three processes per server,
not one:
python.exe (.venv\Scripts) the venv launcher
└ python.exe (base install) what it re-execs — this runs server.py
└ ffpython.exe the FontForge worker
Get-CimInstance Win32_Process -Filter "Name='python.exe' OR Name='ffpython.exe'" |
Where-Object { $_.CommandLine -like "*fontforge-mcp*" } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
Glyphs render as garbage after copying between fonts — TrueType-sourced fonts
are quadratic. Copying into a cubic font through a pen drops the curve data and
flattens everything into polygons. Match is_quadratic on both fonts, or use
FontForge's own copy/paste, which preserves it.
ff_validate reports "missing extrema" right after cleaning — order matters.
round() after addExtrema() nudges points off the true extreme. The order that
works for TrueType is removeOverlap → correctDirection → addExtrema → round,
with rounding last.
correctDirection won't fix a glyph — run removeOverlap on it first.
Direction can't be resolved while contours overlap.
Private Use Area characters come back empty from tools — PUA codepoints don't always survive transport as strings. Pass glyph names, or copy the glyphs into a scratch font mapped to ASCII for rendering.
varLib "succeeds" but a glyph doesn't vary — its masters have different point
counts, so varLib skipped it silently. ff_build_variable_font detects this and
reports the affected glyphs under glifos_que_NAO_variam.
Platform support
Developed and tested on Windows 10/11 with FontForge 20251009. The Linux
and macOS discovery paths are implemented but not yet tested on real machines
— reports and fixes welcome.
License
MIT — see LICENSE.
On the FontForge question, since it comes up: FontForge's own LICENSE states
that the program as a whole is GPL-3.0-or-later, while almost every
individual part carries the 3-clause BSD license (George Williams wrote most
of it under BSD; only some post-2012 contributions are GPLv3). That mix is why
GitHub reports FontForge as NOASSERTION rather than a single SPDX identifier.
This project does not bundle or distribute FontForge — you install it
yourself. worker.py is a standalone script that runs inside whichever
FontForge you already have, and it reaches the server only through a pipe:
separate processes, no linking, no shared address space. That separation is what
makes a permissive license appropriate here.
The license covers this software, not the fonts you build with it. Those are yours.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
E2B
Using MCP to run code via e2b.