Inspiration
I write a lot of math in LaTeX, and the AI workflow for it has been stuck in copy-paste mode: paste the source into a chat, get an edited copy back, paste it into an editor, hit a compile error, paste the error back, wait, repeat. The model never sees the document, only a stale snapshot from three messages ago, and I'm the courier running between them.
Models are also shaky at LaTeX in a way people don't expect. The TeXpert
benchmark puts even frontier models near 50% on compile tasks, and most of
those misses are plain syntax slips, the kind a live renderer catches
instantly. I mistype \alpha as \alpa more often than I'd like to admit.
So does GPT-5.6. The fix the model needs is already on the page, and until
WebMCP nothing could hand it over. I read the spec and started sketching a
block-based pad that same evening, one where the agent and I share a single
live document.
What it does
Lemma is a live LaTeX workspace your agent can edit with you. You type in a CodeMirror source pane, KaTeX renders every block as you type, and the agent (in ChatGPT's in-app browser, Chrome 149+, or Brave) works on the same live document through 15 imperative WebMCP tools plus one declarative form. It can list documents, read the block structure with render status and equation numbers, search, and make surgical edits: update one block by ID, insert labeled equations, leave margin comments, and undo its own changes.
The seeded demo document is an integration by parts derivation whose last
step hides a typo, \alpa where \alpha should be:
$$ I = \int_0^1 x e^x\,dx = e - (e - 1) = 1 $$
Say "fix the render errors" and you watch get_render_errors return the
typo with the block ID, the position, and a suggested fix, then update_block
apply it, and the equation snaps clean with the edit attributed to the agent
and one click to undo. Then ask it to add the product rule
( (fg)' = f'g + fg' ) with a label and a referencing paragraph. The new
equation is numbered (2) and the reference in the new paragraph resolves
live. No copy-paste, no server round trip, everything stays in the tab.
How I built it
Next.js 16 static export, React 19, TypeScript strict mode, Tailwind 4 with a custom token set, CodeMirror 6, KaTeX, Zustand, IndexedDB.
The document model is a list of blocks (preamble, text, equations, theorems, tables), each with a stable ID. Agents edit by ID, never by quote matching, and a fuzzy reconciliation pass keeps IDs anchored when I hand-edit the source underneath them. A document-wide render context does real equation numbering, theorem numbering per kind, and \ref / \eqref resolution, and the tool payloads expose labels and numbers so the agent can cite equations correctly.
The tool layer follows the spec and the security guidance closely:
readOnlyHint on the five read tools, untrustedContentHint on everything
that returns user-authored LaTeX (a math comment can carry a prompt
injection), tight input caps, and abort-aware execute. Errors are returned as
values with a hint, never thrown, because the spec reduces a thrown reason to
a generic UnknownError the agent can't act on. That one decision is what
makes the self-correction loop actually close.
It's tested the boring way: 175 unit tests, plus two end-to-end suites that
drive the real document.modelContext.registerTool and executeTool APIs
(89 calls in one, 1043 calls in a soak test with concurrency, fuzzing, and
hostile inputs, zero thrown exceptions). A built-in Tool Inspector wraps the
same registered tools in UI, so anyone without a WebMCP-capable browser can
still exercise every tool and see the exact JSON schemas.
Challenges I ran into
The ChatGPT browser implements a subset of WebMCP and skips declarative form
tools. My sidebar search was declarative only, so to ChatGPT's browser it
simply didn't exist. I added an imperative search_documents tool and now
nothing depends on the form.
The nastiest bug was silent. A bracket-corrupted netlify.toml dropped my
custom headers on the deployed build with no warning, which meant no
origin-keyed agent cluster, which meant registerTool threw a
SecurityError in production while working perfectly locally. Headers are
now checked on every deploy.
There was also the day Codex told me my tools were "available in Lemma, but not connected to this task's tool interface." I went down the rabbit hole on that one. The spec is clear: tools live in the tab, and only agents that drive a WebMCP-capable browser can reach them. A sandboxed CLI has no bridge. Running the agent in the desktop app's built-in browser fixed it, and that investigation turned into the agent-compatibility matrix in the README.
KaTeX is not LaTeX, and the gaps needed real engine work: \label needs
space-padding so error positions stay honest, tables render through a custom
HTML path with booktabs rules, and errors map back to source positions so the
editor, the preview, and the agent all agree on where the problem is.
Accomplishments that I'm proud of
The self-correction loop is real and closed: write, render, read the
structured error, fix, re-render, attribute, undo. The 1043-call soak test
runs against the live registered tools with zero exceptions, and the app
works in ChatGPT's actual in-app browser, which was the point of the whole
exercise. It also holds up as a product rather than a demo: five document
templates, live numbering and cross references, a mobile layout with a math
key bar, a real print surface, compilable .tex export, and light and dark
themes that got the same care as the editor.
What I learned
Designing tools for an agent is a lot like designing UI for humans. The error payload is the interface: if it says "position 22, suggestion \alpha", the model fixes it in one shot; if it says "compile failed", you get another round trip. Honest annotations matter more than I expected, both for how the agent behaves and for anyone auditing the integration. And tab-bound, ephemeral tools turned out to be a feature for this use case rather than a limitation: a scratch pad that lives and dies with the tab is exactly what a working draft should be.
What's next for Lemma
The next priority is honest LaTeX coverage: expanding what renders correctly without pretending to support what doesn't. More agent support comes right after. Right now the compatibility matrix is ChatGPT's in-app browser, Chrome 149+, and Brave. That's it. I want Lemma working in every browser and desktop agent that ships WebMCP, and I'll keep the compatibility matrix in the README honest as that list grows. On the LaTeX side, the gaps that matter most to users get filled first; the rest stay labeled unsupported rather than silently broken. The spec is still moving, and every new browser that ships WebMCP makes the whole thing more useful without a single line of change on my end.
Built With
- ai-agents
- chatgpt
- chrome
- codemirror
- editor
- indexeddb
- katex
- latex
- llm
- local-first
- math
- mathematics
- netlify
- nextjs
- react
- static-site
- tailwindcss
- typescript
- webmcp
- zustand
Log in or sign up for Devpost to join the conversation.