Skip to main content
Glama

# 🌿 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_..."
      }
    }
  }
}

Related MCP server: claudeleaf

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

Install Server
A
license - permissive license
A
quality
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

View all related MCP servers

Related MCP Connectors

  • Edit your Overleaf LaTeX projects from Claude and ChatGPT; every change is a real Git commit.

  • Connect AI assistants to GitHub - manage repos, issues, PRs, and workflows through natural language.

  • Connect AI assistants to your GitHub-hosted Obsidian vault to seamlessly access, search, and analy…

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/superzeldalink/typleaf-mcp'

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