Skip to main content
Glama
LHxis

fontforge-mcp

by LHxis

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.


Related MCP server: FontLab MCP Server

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:

  1. $FFPYTHON — an explicit override always wins, even when it is wrong, so a bad value fails loudly instead of silently falling through.

  2. Windows registry — FontForge's installer records InstallLocation under the uninstall keys. One lookup finds it on any drive, no scanning.

  3. PATHffpython, fontforge-python, and python3 (not on Windows, where python3 resolves to the Microsoft Store stub and pops the Store UI).

  4. 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.

  5. The running interpreter — a venv built with --system-site-packages may 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_glyph repairs 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.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    -
    quality
    C
    maintenance
    Enables AI agents to diagnose, modify, and validate OTF/TTF fonts interactively through a set of read-only, write, and validation tools.
  • A
    license
    -
    quality
    D
    maintenance
    Enables AI assistants to interact with FontLab for font design and manipulation, including querying font metadata, creating/modifying glyphs, applying transformations, and exporting fonts.
    7
    MIT
  • A
    license
    -
    quality
    C
    maintenance
    MCP bridge for AI-assisted type design in GlyphsApp. Lets Claude, Cursor, or any MCP client read and write font data directly in GlyphsApp — bidirectional, real-time, live in the editor.
    7
    MIT

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LHxis/fontforge-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server