Skip to main content
Glama
README.md
# 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

A3.8/5.0

Scored across 8 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness3/5

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.

Maintenance

ActivitySlowing
ResponsivenessNo issues