overleaf-mcp
# overleaf-mcp
An [MCP](https://modelcontextprotocol.io) server for **self-hosted Overleaf
Community Edition**. Lets a coding agent create projects, write LaTeX, compile,
and read back *structured* diagnostics — while a human edits the same document
in the browser.
```
compile → {
"verdict": "FAILED with 1 fixable LaTeX error(s)",
"errors": [{
"file": "./main.tex", "line": 4,
"message": "Is \\usepackage{natbib} missing?",
"fixable": true
}]
}
```
The raw log said `! Undefined control sequence.`
---
## Why this exists
Existing Overleaf MCP servers target **overleaf.com** and authenticate one of
two ways:
- **Git bridge** — the most popular ones. Unusable on Community Edition: the
git bridge is a Server Pro feature. The CE image neither builds nor starts it
(absent from `server-ce/services.js`, `server-ce/runit/`, and the Dockerfile),
and `Features.mjs` gates it on `Settings.enableGitBridge`, which CE never sets.
- **Session cookies / headless browser** — works, but sessions expire, CSRF has
to be handled, and login-flow changes break it.
Self-hosting opens a third door that overleaf.com users cannot reach, because it
is an internal interface there: Overleaf's own **`privateApiRouter`**,
authenticated with plain HTTP Basic. No session, no CSRF, no browser.
## What it does differently
**Structured diagnostics, not raw logs.** Vendors Overleaf's own log parser and
its 1200-line ruleset — years of accumulated knowledge translating cryptic TeX
messages into actionable ones, including a command→package map (`\citep` →
`natbib`, `\toprule` → `booktabs`).
**Separates "fix your LaTeX" from "fix your server."** A missing package is not
something an agent can solve by editing source. Conflating the two is what makes
agents loop until they run out of context — the failure mode
[The AI Scientist](https://arxiv.org/abs/2408.06292) reports as a central
bottleneck. Compare:
```
verdict: FAILED with 1 fixable LaTeX error(s) — see errors[] for file/line.
verdict: BLOCKED: the server environment cannot build this document —
missing package/class file hyperxmp.sty. Install it on the server
(tlmgr install ...); the agent cannot fix this in source.
Do not retry without changing the server.
```
Both are "compile produced no PDF." Only one is worth another attempt.
## Architecture
Two backends, because neither alone suffices:
| Concern | Backend | Why |
|---|---|---|
| Project CRUD | `api` process `:3000` | Stable HTTP Basic interface — the one Dropbox/GitHub sync uses |
| Compilation | CLSI `:3013` | Returns `output.log`. The web API's compile endpoint obtains a `buildId` internally and **never exposes it**, so diagnostics are unreachable through it |
Writes enter through `EditorController.upsertDocWithPath` — the same entry point
the web UI uses. For an existing document that reaches
`DocumentUpdaterHandler.setDocument`, which diffs old against new and applies the
result as an **operational transform**. An agent write is merged into the live
document exactly like a keystroke: a human with the project open sees it
immediately, their cursor survives, and it lands in project history.
Verified against a live instance: with the document loaded in document-updater,
an agent write moved it from `version 0` to `version 1` — an incremental
operation, not a reload.
## Requirements
- Self-hosted Overleaf **Community Edition** (Server Pro works too)
- Shell access to the host running it
- Node.js ≥ 20 wherever the MCP server runs
- Overleaf reachable over HTTPS
## Setup
### 1. Enable agent access on the server
```bash
git clone https://github.com/lintheyoung/overleaf-mcp
cd overleaf-mcp
# Kubernetes / k3s (Zeabur, Coolify, ...)
bash deploy/setup.sh --k8s --namespace <ns> --deployment <name>
# docker compose — not yet verified end to end, see Caveats
bash deploy/setup.sh --docker --container sharelatex
```
Find the namespace and deployment with
`kubectl get deploy -A | grep -i overleaf`; for compose it is the service name
in `docker-compose.yml` (usually `sharelatex`).
This generates secrets under `/etc/overleaf-agent`, injects them so Overleaf
uses them, and installs an nginx reverse proxy exposing two prefixes:
| Path | Upstream | Guard |
|---|---|---|
| `/agent-api/` | Overleaf api `:3000` | HTTP Basic (app) + path allow-list (nginx) |
| `/agent-clsi/` | CLSI `:3013`, artifacts `:8080` | `X-Agent-Token` (nginx) |
It mounts at `/etc/nginx/vhost-extras/overleaf/`, an include directory the stock
Overleaf vhost already provides — no vendor file is overwritten.
> [!CAUTION]
> **CLSI has no authentication of its own.** It runs arbitrary LaTeX, which on
> Community Edition means arbitrary code execution inside the container with
> filesystem and network access. The token gate is not optional hardening; it is
> the only thing standing in front of it. Never expose `:3013` directly.
The allow-list matters too: `privateApiRouter` also carries endpoints for user
expiry and project deactivation. Only what an agent needs is routed; everything
else under `/agent-api/` returns 404 even with valid credentials.
### 2. Survive redeploys (managed platforms)
Platforms like Zeabur own the Deployment. A *restart* keeps the configuration
above; a *redeploy* rebuilds from the platform's spec and silently drops it —
the API starts answering 401 and the routes 404.
```bash
sudo install -m 700 deploy/restore.sh /usr/local/sbin/overleaf-agent-restore.sh
sudo install -m 644 deploy/systemd/* /etc/systemd/system/
sudo systemctl enable --now overleaf-agent-restore.timer
```
Runs 3 minutes after boot, then hourly. It is a **no-op when nothing is
missing** — patching a Deployment triggers a rollout, so an unconditional timer
would restart Overleaf every hour.
### 3. Build and register the server
```bash
npm install && npm run build
```
`npm run vendor` (invoked by `build`) fetches Overleaf's log parser from GitHub.
See [Licensing](#licensing).
Find the Overleaf user id that should own agent-created projects — visible in
the URL when viewing that user in the admin panel, or from the database.
```json
{
"mcpServers": {
"overleaf": {
"command": "node",
"args": ["/path/to/overleaf-mcp/dist/index.js"],
"env": {
"OVERLEAF_BASE_URL": "https://overleaf.example.com",
"OVERLEAF_API_USER": "overleaf",
"OVERLEAF_API_PASS": "<from /etc/overleaf-agent/api_pass>",
"OVERLEAF_CLSI_TOKEN": "<from /etc/overleaf-agent/clsi_token>",
"OVERLEAF_OWNER_ID": "<overleaf user id>"
}
}
}
}
```
### 4. Verify
```bash
node dist/dev/selftest.js # offline — diagnostics layer only
node dist/dev/smoke.js # end-to-end against the live instance
```
`smoke` creates a project, writes broken LaTeX, checks the diagnostic names
`natbib`, applies the fix, and confirms a PDF comes out.
## Configuration
| Variable | Required | Default | Meaning |
|---|---|---|---|
| `OVERLEAF_BASE_URL` | yes | | e.g. `https://overleaf.example.com` |
| `OVERLEAF_API_USER` | yes | | `WEB_API_USER` (usually `overleaf`) |
| `OVERLEAF_API_PASS` | yes | | `WEB_API_PASSWORD` |
| `OVERLEAF_OWNER_ID` | yes | | Overleaf user id owning created projects |
| `OVERLEAF_CLSI_TOKEN` | for diagnostics | | Must match the nginx config |
| `OVERLEAF_API_PATH` | no | `/agent-api` | |
| `OVERLEAF_CLSI_PATH` | no | `/agent-clsi` | Empty disables diagnostics |
| `OVERLEAF_TIMEOUT_MS` | no | `120000` | Raise for long bibliographies |
Without `OVERLEAF_CLSI_TOKEN` the server still runs, but only `get_pdf` works —
no diagnostics.
## Tools
| Tool | Notes |
|---|---|
| `create_project` | Returns id + web URL |
| `list_files` / `read_file` | Reads back from Overleaf, so human edits are visible |
| `write_file` | Whole-file replace, merged as an OT |
| `delete_file` | |
| `compile` | Structured diagnostics. **Does not** return the PDF |
| `get_pdf` | Writes the PDF to disk, returns the path |
| `get_log` | Raw log, tail-truncated. Last resort |
`compile` withholds PDF bytes deliberately — base64 of a paper would swamp an
agent's context for no benefit.
## Working alongside a human
OT guarantees no data is lost, not that the result is what you wanted:
> The agent reads `intro.tex` at T0. A human rewrites a paragraph at T1. The
> agent writes back its T0-derived text at T2. The human's edit is gone from the
> live document — recoverable from history, but they will not know to look.
Split the paper and give each file an owner:
```
main.tex skeleton, rarely touched
sections/intro.tex agent
sections/related.tex human
```
Physical separation beats relying on merge semantics. See [CLAUDE.md](CLAUDE.md)
for rules to hand your agent.
## TeX Live packages
`deploy/tlget.sh` installs packages into `TEXMFHOME` by unpacking tlnet archives
directly, working around two `tlmgr` behaviours that bite containerised
Overleaf:
- *"package X is not relocatable, cannot install it in user mode"* — tlmgr
refuses to place some packages under `TEXMFHOME`, the only TeX tree on the
persistent volume. The system tree is inside the image and is wiped on every
restart. `hyperxmp`, required by `acmart`, is one of these.
- *"tlmgr itself needs to be updated"* — CTAN's tlnet only carries the current
release, so once upstream moves on the tlmgr baked into the image cannot
install anything at all.
```bash
bash deploy/tlget.sh <namespace> <deployment-substring> hyperxmp
```
A tlnet archive is just a tarball of a `texmf-dist` tree, so unpacking it into
`TEXMFHOME` sidesteps both problems and lands the files on the volume.
## Licensing
This project is **MIT** (see [LICENSE](LICENSE)).
`src/vendor/` is **not** part of it and is **not committed**. `npm run vendor`
fetches five files from [overleaf/overleaf](https://github.com/overleaf/overleaf)
at build time; those remain **AGPL-3.0** and belong to Overleaf. Keeping them
out of the repository is what keeps the licences separate — otherwise AGPL's
network-use clause would extend to everyone running this server.
To pin or audit the fetch:
```bash
OVERLEAF_REF=v5.5.4 npm run vendor # pin to a tag
npm run vendor -- /path/to/overleaf # use a local checkout
```
Overleaf is a trademark of Overleaf Inc. This project is not affiliated with or
endorsed by them.
## Caveats
- **Community Edition has no compile sandbox.** Any logged-in user can read
container environment variables through LaTeX. Upstream says as much. Only
give accounts to people you trust, and do not open registration.
- `write_file` replaces whole files. There is no patch/append tool yet.
- Compilation is synchronous. A bibliography-heavy document takes 20 s+; CLSI's
own ceiling is 600 s.
- Verified against Overleaf CE 5.x on k3s. The `--docker` path in `setup.sh` is
written but **not yet tested end to end** — it also cannot inject the
environment variables automatically, so that step is manual. Reports welcome.
TDQS
Scored across 8 tools
Each tool targets a distinct operation: project creation, file listing, reading, writing, deleting, compiling, PDF retrieval, and log access. Even the compile/get_pdf/get_log trio is clearly separated by their descriptions (structured diagnostics vs. artifact bytes vs. raw log). No two tools are likely to be confused.
Most tools follow a clear verb_noun snake_case pattern (create_project, list_files, read_file, write_file, delete_file, get_pdf, get_log). 'compile' is a single verb without an object, which is a slight deviation, but the naming remains consistent and predictable overall.
Eight tools is well-scoped for an Overleaf/LaTeX project server, covering project creation, file management, compilation, and output handling. Each tool serves a clear purpose without unnecessary bloat.
The file-level CRUD (list/read/write/delete) and compile/PDF/log operations are well covered. However, the project lifecycle is incomplete: there is no way to list existing projects, delete a project, or update project metadata, which is a notable gap for a multi-project server.