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

Share this project:

Updates

Submission history