Skip to main content

🌿 Typleaf MCP Server

A Model Context Protocol (MCP) server for Typst projects on Typleaf Pro — a self-hostable Overleaf fork with Typst support.

28 tools covering full CRUD, document structure analysis, git history & diff, compilation, PDF download, PDF-layout perception, and citation verification.

Typleaf compiles both Typst and LaTeX, so this server handles both. The right parser is chosen from the file extension — you never have to say which.

You: "What kind of project is 6a6dfc57bbb3aac01ed9a71d?"
AI:  [project_info] Typst, root main.typ, 36 .typ files, bibliography refs.yml

You: "Read sections/03-method.typ"
AI:  [read_file] Here's the content: …

You: "Tighten the Background section"
AI:  [update_section] ✓ Edited and pushed

You: "Compile it and tell me how many pages"
AI:  [get_page_count] 61 pages (format=typst, source=pdf)

You: "Which page does each heading start on?"
AI:  [section_page_map] p.1  = Introduction … p.8  == Parameters …

This is a fork of overleaf-mcp-plus by rangehow, retargeted at self-hosted Typleaf. See What changed from the upstream.


🚀 Setup

1. Install

pip install "typleaf-mcp[compile]"

Or run without installing:

uvx --from "typleaf-mcp[compile]" typleaf-mcp

2. Get your credentials

Variable Required for Where to get it
TYPLEAF_BASE_URL everything Your instance's URL, e.g. https://typleaf.example.com
TYPLEAF_SESSION everything else — read, write, compile, PDF DevTools → Application → Cookies → your instanceoverleaf.sid → Value
TYPLEAF_GIT_TOKEN optional — commit history only <base>/user/settings → Git Integration → Create Token

You do not need a git token. Typleaf's git bridge is an optional module, and many deployments don't run it — on those, no token exists to set. This server reads and writes with the session cookie alone (see Backends). Only list_history, get_diff and sync_project need git, and they explain the gap rather than failing cryptically.

Cookie name: Overleaf ships two. Community Edition — which Typleaf is built on — sets overleaf.sid; the hosted service sets overleaf_session2. Guessing wrong looks exactly like an expired session, so this server sends both names with your value unless you pin one via TYPLEAF_SESSION_COOKIE. Just copy whichever your instance shows.

TYPLEAF_BASE_URL has no default, on purpose. This server sends your session cookie and git token to whatever host it is pointed at. Defaulting to www.overleaf.com — as the upstream does, correctly, for a single-host service — would mean an unconfigured install leaks a private instance's credentials to a third party. So it errors instead. The OVERLEAF_* spellings of all three are accepted as fallbacks, so an existing overleaf-mcp config only needs its base URL changed.

The session cookie is HttpOnly: you must copy it from the DevTools Cookies panel, not the JS console.

Git tools additionally need the git-bridge module enabled on your deployment. Without it, clones 404 even though every project is visible in the web UI — the error message says so.

3. Register the server

{
  "mcpServers": {
    "typleaf": {
      "command": "typleaf-mcp",
      "env": {
        "TYPLEAF_BASE_URL": "https://typleaf.example.com",
        "TYPLEAF_SESSION": "s%3A...",
        "TYPLEAF_GIT_TOKEN": "olp_..."
      }
    }
  }
}

🔌 Backends

The server picks its backend from what your deployment actually offers:

web (cookie only) git (bridge enabled)
Selected when no TYPLEAF_GIT_TOKEN token is set
Read / write files
Commit messages ❌ — edits land as ordinary project changes
list_history / get_diff / sync_project

status_summary reports which one is live.

How the web backend writes. There is no HTTP endpoint that sets a document's content — setDocument exists but sits behind service-to-service auth, and the editor itself writes over WebSocket using operational transform. But POST /Project/<id>/upload is cookie-authenticated and upserts: uploading a name that already exists replaces that entity in place, keeping its id, and a text file comes back as an editable doc rather than a binary attachment.

The catch is that upload needs a folder_id, and no HTTP endpoint exposes folder ids — /entities gives paths only, /metadata gives doc ids only, and creating a folder that exists returns 400 file already exists. They arrive only in the real-time service's joinProject payload, over socket.io 0.9 — a protocol no maintained Python client speaks. So realtime.py implements the four frame types needed to read the tree, and writes go over plain HTTP afterwards. That sidesteps implementing operational transform, which is the genuinely hard part of talking to Overleaf's editor.

Editing an existing document goes through operational transform, not upload. An edit is sent as the operation{"p": 12, "d": "Original"}, {"p": 12, "i": "EDITED"} — down the same WebSocket the editor uses, so a collaborator with the file open sees it appear in place. Verified with a second client watching: it receives the op live. A one-word change in a 2,430-character file transmits 7 characters, so cursors, selections and the track-changes attribution of untouched text all survive.

Upload is still used where there is no document to edit yet — new files and binary assets.

One wrinkle worth documenting, since it looks alarming in logs: this server's socket.io 0.9 stack sends its ack in a frame that sets the MASK bit, which RFC 6455 forbids a server to do, and strict clients close the connection rather than read it — after the edit has already applied. So the write is confirmed by reading the document back, not by the ack. That's a stronger check anyway: it tests the result rather than the transport.

Multi-file changes are all-or-nothing. Use write_files rather than looping over rewrite_file. Every edit is validated and resolved before anything is written, so the common failures — missing file, search string matching twice or not at all, duplicate path, bad name — touch nothing at all. If a write still fails, the files already written are restored.

It's a compensating transaction, not a real one (Typleaf has no multi-document transaction and no cookie-reachable version restore), so two limits are reported rather than hidden: rollback can itself fail, in which case the result names exactly which files are in which state; and intermediate writes were real, so a collaborator watching may have seen a state that was later undone.

🎯 Typst specifics

The compiler setting is what makes a project Typst

Typleaf's CLSI generates a Typst sync map only when the project's compiler is typst and the root document ends in .typ. A project full of .typ files that is still on the default pdflatex fails to compile with TeX errors that never mention Typst, and every PDF-position tool silently returns nothing.

You: "Make a new Typst paper"
AI:  [create_project name="Paper" compiler="typst"] ✓

or on an existing project: set_compiler(project_id, "typst").

Headings

get_sections, update_section and section_page_map recognise =-markup headings at column 0 — exactly the rule Typleaf's own editor outline uses, so the sections this server reports are the ones you see in the IDE's outline pane. Raw blocks, line comments and (nested) block comments are skipped, so a = inside a code sample is not mistaken for a section.

#heading(level: n)[…] calls are found by project_info but are deliberately not editable via update_section — rewriting the body of a programmatically generated heading is not a text-span operation.

Compile logs

Typst writes no log file; it reports everything on stderr, which CLSI captures into output.log. download_log parses those diagnostics into a compact list with file:line:column:

Typst compile log — 1 error(s), 1 warning(s)

  ✗ expected comma  (sections/05-parameters.typ:13:74)
  ⚠ unknown font family: calibri

Pass raw=true for the unparsed text.

PDF-position tools, and their honest limits

locate_in_pdf and section_page_map work for Typst, backed by Typleaf's TypstSyncManager rather than SyncTeX. Four differences are real and are reported rather than hidden:

  • Coarser. Typleaf injects zero-width #metadata markers into the sources after compiling and asks typst query where they landed. Anchors are per source line, but only where a marker was safe to place — a lookup resolves to the enclosing block. Content in #let/#show template bodies gets no anchor at all, and a #for body gets one anchor for the whole loop.
  • Slower. output.typst-sync.json is listed among the compile outputs but the web tier will not proxy it (verified: it 404s while output.pdf from the same build serves fine), so there is nothing to parse offline. A LaTeX project resolves every heading from one output.synctex.gz parse; a Typst project costs one request per heading. section_page_map is therefore capped at 150 headings by default (TYPLEAF_MAX_SECTIONS) and says so when the cap bites.
  • Headings are queried via their body prose, not their own line. A document with #outline() renders every heading twice, and Typleaf's sync map keeps whichever copy fits the file's document-order trajectory — which fails for the first heading in each file, since there is no prior trajectory. Measured on a real 104-page document, = Functions resolved to page 2 (the contents) against a true page of 12. Prose is never duplicated into an outline, so that is what gets queried. Any residual backwards jump is flagged ! in the output rather than presented as fact.
  • No text-area figures. text_area_fill_pct / text_area_remaining_pt come from the LaTeX geometry package's log dump. Typst reports no page geometry, so those fields are absent for Typst rather than guessed. Physical-page fullness (from the PDF MediaBox) is still reported.

section_page_map follows #include through the whole document by default — a Typst root is usually a thin index whose own heading count is zero. Pass file to map a single file instead.

Page counts for Typst are measured by parsing the PDF, since typst prints no "Output written on … (N pages)" line. And a Typst compile that produces no diagnostics writes no log at all, which download_log reports as success rather than as a missing file.

Bibliographies

Typst reads BibTeX and its native Hayagriva YAML. verify_citations handles both: .yml entries are translated to the minimal BibTeX the verifier reads (title, DOI, arXiv id, author, year, journal — the fields a verdict actually depends on).

Discovery differs from the LaTeX side for a reason: .yml is an ambiguous extension, and a bare scan would feed a GitHub Actions workflow to the verifier. So for Typst the tool reads the #bibliography(…) call out of the source — walking the #include graph, since a #bibliography in a back-matter.typ is a common layout — and falls back to a .bib scan only if none is declared. The report says which route it took.

For Typst projects it also lists cited-but-undefined keys, which render as broken ? references. LaTeX shouts about these at compile time; Typst's warning is easy to miss. Label cross-references (@fig-plot pointing at <fig-plot>) are excluded project-wide, so a well-labelled document does not report its own labels as missing citations.


🛠 Tools (29)

Orientation

Tool Description
project_info Format (Typst/LaTeX), compile root, file counts, declared bibliography. Cheap — no compile. Call this first.
list_projects All projects on the instance
status_summary Format, file counts, heading structure of the root

Read

Tool Description
list_files List files, optionally filtered by extension
read_file Read a file's contents
search_files Regex search across every text file — one request regardless of project size
get_sections Heading structure with hierarchy and previews
get_section_content One section's full text by title
verify_citations Verify DOIs/arXiv ids against CrossRef & arXiv; BibTeX + Hayagriva

Write

Tool Description
create_project New project, optionally with compiler="typst"
create_file New file; auto-creates parent folders
edit_file Surgical exact search-and-replace (like sed)
rewrite_file Replace a file's entire contents
update_section Replace a section's body, preserving its heading
write_files Apply a coordinated change across several files, all-or-nothing
upload_file Upload a local binary (images, PDFs)
delete_file Delete a file
set_compiler Switch the project's compiler (typst, pdflatex, …)

History

Tool Description
list_history Commit log, filterable by file and date
get_diff Diff between refs or the working tree
sync_project Pull the latest changes

Compile & output

Tool Description
compile_project Trigger a compile; returns status + output files
download_pdf Save the compiled PDF locally
download_log Compile log — parsed Typst diagnostics, or the raw TeX log
download_source_zip Save the project source as a .zip
download_source Extract the project source into a directory

Layout

Tool Description
get_page_count Total pages in the compiled PDF
locate_in_pdf Where a source line lands: page + rectangles
section_page_map Every heading → its page, plus last-page fullness

All writes commit and push immediately. Every tool is annotated with MCP safety hints (readOnlyHint / destructiveHint / idempotentHint), and a startup check refuses to run if any tool is left unclassified.


🔄 What changed from the upstream

overleaf-mcp-plus targets hosted overleaf.com and LaTeX. The retarget touched five areas:

New — Typst support

  • typst.py — heading, include/import, bibliography and citation parsing, matching Typleaf's own editor rules
  • document.py — extension-based dispatch so every tool serves both formats from one code path
  • typst_log.py — parser for typst compile diagnostics
  • hayagriva.py.yml bibliography → BibTeX for the verifier
  • layout.py — a second backend using Typleaf's Typst sync map, with the SyncTeX path untouched for LaTeX
  • New tools: project_info, set_compiler; create_project gained a compiler argument

Self-hosting

  • TYPLEAF_BASE_URL is required, with no default (see above)
  • The git bridge is on the instance's own origin at /git/<id>, not a separate git.overleaf.com host
  • Clone failures name the git-bridge module as a possible cause, with the token redacted from the URL
  • The local-copy sidecar records which instance a checkout came from, not just the project id

Writes that an open editor survives

Upstream replaces a file over HTTP. That works, but any collaborator with the file open gets "this file has gone out of sync" and loses their place. This fork edits through the real-time channel instead — a minimal ShareJS diff, so the change streams into open editors the way a human's typing does.

  • ot.py — minimal insert/delete components, emitted in reverse document order so every offset stays valid, and replayed locally before transmission
  • realtime.py / polling.py — socket.io 0.9 clients (WebSocket and xhr-polling). Typleaf's git bridge is an optional module, so on an instance without it the real-time service is the only place folder and document ids exist
  • transaction.py — multi-file edits validated up front and rolled back on partial failure, so a broken compile is not the way you learn a write failed
  • search_files — regex search server-side, instead of listing and reading candidates one at a time

The awkward one is an encoding bug in Typleaf's real-time service: it serves document lines decoded as Latin-1 while its own ShareJS offsets are over the correctly-decoded string. Diff against the text as delivered and every offset past the first non-ASCII character is wrong — inserts validate nothing and land silently in the wrong place, deletes cannot match and the server drops the connection. Pure-ASCII files are byte-identical either way and work perfectly, which makes it look like a size or permissions fault. ot.decode_doc_text undoes the rendering; it is idempotent, so it stays correct if the instance is ever fixed to serve UTF-8.

Bug fixes carried into the fork

  • download_log was unreachable upstream: a bad merge left its return statements orphaned inside section_page_map, so the tool fell through to Unknown tool: download_log
  • The credential guard now reports a missing base URL rather than only the missing cookie

Kept as-is — git client, LaTeX parser, SyncTeX engine, thread-safe per-project locking, MCP SDK v2 handler registration, tool safety annotations.


🧪 Development

git clone <this repo> && cd typleaf-mcp
python -m venv .venv && .venv/bin/pip install -e ".[compile]" pytest
.venv/bin/python -m pytest tests -q

391 tests, no external network. tests/conftest.py points the package at a .invalid host, so any test that escapes its stubs fails with a DNS error instead of reaching a live server.

tests/test_integration_fake_instance.py runs a stand-in Typleaf on a real socket and asserts on the requests it receives — that the session cookie is sent, that writes carry the CSRF token, that per-build output URLs carry ?clsiserverid, that sync/code gets a plain project path, and that no .synctex.gz is ever requested for a Typst project. Those are the wire details a mocked httpx cannot cover, and where this package's bugs historically live. It is the slow part of the suite (~18s) because each call opens a fresh connection.

tests/test_verify_citations.py skips unless tofu-search is installed; the discovery and reporting logic this fork adds is covered separately in tests/test_verify_discovery.py, which has no such dependency.


📄 License

MIT. Not affiliated with Overleaf, Inc., Digital Science, or the Typst project. Typleaf Pro is an independent community project.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

typleaf_mcp-0.1.0.tar.gz (115.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

typleaf_mcp-0.1.0-py3-none-any.whl (129.8 kB view details)

Uploaded Python 3

File details

Details for the file typleaf_mcp-0.1.0.tar.gz.

File metadata

  • Download URL: typleaf_mcp-0.1.0.tar.gz
  • Upload date:
  • Size: 115.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.1

File hashes

Hashes for typleaf_mcp-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6000a33d85b4fa9648b10219ccdd2f56a13ad811a0d0d30a72d2d3ea4696b8d1
MD5 4097808e0186ffcf37abeb95896817fe
BLAKE2b-256 a09aeb80f3b12fabe945009d21851b84c2d21733eb8a63164d3a79d346c168fc

See more details on using hashes here.

File details

Details for the file typleaf_mcp-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: typleaf_mcp-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 129.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.1

File hashes

Hashes for typleaf_mcp-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e7bf26b1dcc5544acacdc1e393e03adddaad268349b7e07f3bd86a2c90cc248f
MD5 cd39f1e058348451a40842b8980da435
BLAKE2b-256 f46804de3918854e698d9f3f4b8caef03aa99d9732af7f6f09882f4d84a36153

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page