Skip to content

Command line

Every tool is also a command. overleaf-web-mcp reaches the same 31 operations as the MCP server, through the same service, checks, and safety defaults, and prints one JSON result whose exit code says what is safe to do next. Use it from a shell, a script, a CI job, or a coding agent that works in a terminal; no MCP client is involved. Three Agent Skills build on it.

Install and sign in

You need Node.js 20 or newer and, once, a Chrome-family browser for the sign-in.

npx overleaf-web-mcp login
npx overleaf-web-mcp call auth_status

login opens a browser window and saves the session, as in the installation guide. call auth_status then prints the account's project count and when the session expires, and exits 0. For frequent use, install the package once with npm install -g overleaf-web-mcp and run overleaf-web-mcp directly. In a script, pin the version and pass --yes, as in npx --yes overleaf-web-mcp@0.7 projects list: without --yes, npx can print an install notice on stderr the first time.

Commands read the same environment variables as the server, from the shell they run in; see configuration. The session lapses after five idle days unless something uses it; schedule overleaf-web-mcp keepalive to keep it alive (how).

Commands

This is the list overleaf-web-mcp --help prints:

overleaf-web-mcp serve
overleaf-web-mcp login
overleaf-web-mcp keepalive
overleaf-web-mcp --help

overleaf-web-mcp call <tool_name> [--request <file> | --request - | --json <object>] [--progress]
overleaf-web-mcp schema <tool_name> (--input | --output)
overleaf-web-mcp capabilities [projectId]

overleaf-web-mcp projects list [--query <q>] [--limit <n>] [--sort lastUpdated|name] [--include-archived] [--include-trashed]
overleaf-web-mcp tree <projectId>
overleaf-web-mcp files read <projectId> <path>
overleaf-web-mcp files write <projectId> <path> --revision <r> (--content-file <f> | --stdin) [--tracked]
overleaf-web-mcp files download <projectId> <path> <localPath> [--overwrite]
overleaf-web-mcp sync plan <projectId> <localFolder> [--destination <path>] [--ignore <glob>]... [--verbose] [--progress]
overleaf-web-mcp sync apply <projectId> <localFolder> --mode additive|mirror (--plan-token <t> | --unplanned) [--destination <path>] [--ignore <glob>]... [--confirm-delete-count <n>] [--tracked] [--stop-on-error] [--progress]
overleaf-web-mcp compile run <projectId> [--root <path>] [--draft] [--stop-on-first-error] [--download <localPath>] [--overwrite]
overleaf-web-mcp compile log <projectId> --build-ref <r> [--tail <n>] [--kind latex|bibtex] [--errors-only]
overleaf-web-mcp compile download <projectId> <localPath> --build-ref <r> [--file <f>] [--overwrite]
overleaf-web-mcp comments list <projectId> [--status open|resolved|all] [--file <path>] [--author <a>]

serve, login, keepalive, and --help are the server and session commands; serve is the default when no command is given. Everything else is an operation command.

call <tool_name> reaches every operation; the names are those in the tool reference. schema and capabilities describe the operations without calling them. The rest are shorthand: each only builds a tool's input, and its result, errors, and exit code are those of call <tool> with the same input. Anything a shorthand does not cover, such as timeoutMs on a compile, goes through call. --help or -h anywhere in an operation command prints the help and exits 0.

Command Tool Arguments and flags become
projects list list_projects --query query, --limit limit, --sort sort, --include-archived includeArchived: true, --include-trashed includeTrashed: true
tree get_project_tree <projectId> projectId
files read read_file <path> filePath
files write write_file <path> filePath, --revision revision, --content-file localPath, --stdin content, --tracked writeMode: "tracked"
files download download_file <path> filePath, <localPath> localPath, --overwrite overwrite: true
sync plan plan_sync <localFolder> localFolderPath, --destination destinationFolderPath, each --ignore one ignore pattern, --verbose verbose: true
sync apply sync_directory as sync plan, plus --mode mode, --plan-token planToken, --unplanned unplanned: true, --confirm-delete-count confirmDeleteCount, --tracked writeMode: "tracked", --stop-on-error stopOnError: true
compile run compile_project --root rootFilePath, --draft draft: true, --stop-on-first-error stopOnFirstError: true; --download and --overwrite are below
compile log get_compile_log --build-ref buildRef, --tail tail, --kind kind, --errors-only format: "errors-only"
compile download download_compile_output <localPath> localPath, --build-ref buildRef, --file file, --overwrite overwrite: true
comments list list_comments --status status, --file filePath, --author author

A few rules the table does not show:

  • files write takes exactly one of --content-file and --stdin. --content-file is passed as write_file's localPath, so the operation reads the file itself and OVERLEAF_LOCAL_READ_ROOTS applies. --stdin reads all of stdin as UTF-8 text; a leading byte order mark is dropped and invalid UTF-8 is refused.
  • A planToken is bound to the local folder, --destination, and --ignore, so sync apply needs the same values sync plan was given. Mirror mode needs --confirm-delete-count.
  • compile log and compile download require --build-ref.
  • --limit, --tail, and --confirm-delete-count take whole numbers.
  • An unknown option, a missing value, or the wrong number of arguments is a usage error: exit 2, with a message that names the option but never repeats a value.
  • Local paths, here and in requests, resolve against the shell's working directory.

Requests

call <tool> takes the tool's input as one JSON object, the same object an MCP client sends as the tool's arguments:

Flag The request comes from
--request <file> the file
--request - stdin
--json '<object>' the argument itself
none nothing: the input is {}, for a tool without parameters such as auth_status

--json puts the request in the process's arguments, which process listings and shell history show. Keep it for small requests without document text. Document text, and comment text, goes through --request <file>, --request -, files write --content-file, or files write --stdin. A request that is not valid JSON, or not an object, is INVALID_ARGUMENT, and the message says only that; it never quotes the request. An unreadable request file is INVALID_ARGUMENT with details.errno. When stdin is a terminal, a command that would read it is refused with INVALID_ARGUMENT: an operation command never waits for typed input.

Input is strict

The input is checked against the tool's input schema before any session is opened, so a refused call sends nothing and exits 2:

  • A key the schema does not list, at any depth, is INVALID_ARGUMENT with details.unknownKeys, dotted paths such as files.0.localpth.
  • A value the schema refuses is INVALID_ARGUMENT with details.issues, each { path, message }.

Defaults are then applied as the MCP server applies them, so the operation receives exactly what it would through MCP. overleaf-web-mcp schema <tool> --input prints the parameters.

The MCP server is more lenient about unknown keys. The MCP SDK checks arguments against the same schemas but drops a key it does not know without an error, so through MCP a misspelled writeMod: "tracked" is ignored and the edit is written untracked, the default. The command line and the TypeScript SDK refuse it:

$ cat edit.json
{ "projectId": "0123456789abcdef01234567", "filePath": "main.tex", "revision": "opaque-revision-from-files-read",
  "localPath": "main.tex", "writeMod": "tracked" }
$ overleaf-web-mcp call write_file --request edit.json
{"code":"INVALID_ARGUMENT","message":"write_file does not accept the parameter writeMod. Run `overleaf-web-mcp schema write_file --input` for the parameters it takes.","retryable":false,"details":{"unknownKeys":["writeMod"]}}
$ echo $?
2

A value the schema refuses differs in form too: the MCP SDK answers it before the server runs the tool, with its own plain-text error (Input validation error: Invalid arguments for tool ...) and no JSON code, where the command line and the SDK report INVALID_ARGUMENT with details.issues.

Output

On success an operation command prints exactly one JSON value on stdout, indented, with a trailing newline: the tool's result, the same object the MCP server returns as structuredContent and the SDK method resolves with. stderr stays empty unless --progress was given.

On failure stdout stays empty, and stderr gets exactly one line, { "code", "message", "retryable", "details"? }, the same error the MCP server returns. Messages never repeat option values or request content; they can name local paths you supplied. The codes are listed in the safety model.

Progress

With --progress, on call, sync plan, or sync apply, operations that report progress write one JSON line to stderr per step:

{"progress":{"progress":3,"total":12,"message":"Handled 3 of 12 files"}}

plan_sync, sync_directory, batch_upload, and delete_entities report progress; other operations write none. Messages hold counts only, never file names. The result still goes to stdout as one JSON value; on failure the error is the last line on stderr.

Server and session commands

serve writes nothing but MCP protocol frames to stdout, and diagnostic lines naming a tool and an error code to stderr. --help prints plain text. login and keepalive print one JSON value on stdout; login also writes plain-text status lines to stderr while it waits for the sign-in. These commands exit 1 on any failure, with the error as one JSON line on stderr; so does an unknown command. The exit codes below are for operation commands only.

Exit codes

Exit Meaning Error codes
0 Complete.
1 Failed for another reason; read the error code. for example NOT_FOUND, PROTOCOL_UNSUPPORTED, REMOTE_ERROR, COMPILE_FAILED, COMPILE_TIMEOUT, NO_BUILD, BUILD_NOT_FOUND, DOC_TOO_LARGE, UPDATE_TOO_LARGE, PARTIAL_CLEANUP
2 Fix the call: a usage error, or an argument, confirmation, or local path was refused. Nothing was changed. usage errors, INVALID_ARGUMENT, CONFIRMATION_MISMATCH, PATH_OUTSIDE_ROOT
3 Not allowed or not available; do not retry. AUTH_EXPIRED means run overleaf-web-mcp login. AUTH_EXPIRED, PERMISSION_DENIED, POLICY_DENIED
4 Read or plan again, then decide: the document or project changed, a write is known not to have applied, or part of it applied. REVISION_CONFLICT, REMOTE_DRIFT; TIMEOUT with details.outcome: "not_applied" from an operation that is not read-only; any error with details.created: true or a non-empty details.applied from one
5 Outcome unknown: a write may have applied. Read before acting; never re-run it blindly. OUTCOME_UNKNOWN; TIMEOUT from an operation that is not read-only, unless details.outcome is not_applied
6 Partial: some steps completed. The result is on stdout with status: "partial". none: a batch_upload, sync_directory, or delete_entities result with status: "partial", or compile run --download whose download failed
7 Safe to retry later: nothing was changed. RATE_LIMITED, COMPILE_RATE_LIMITED, COMPILE_UNAVAILABLE; TIMEOUT from a read-only operation

Four rules reach beyond the codes in the table, and one rule overrides everything:

  • details.outcome: "unknown" wins. Any error that carries it exits 5, whatever its code, a REVISION_CONFLICT included. This is checked first.
  • A write that partly applied exits 4. An error with details.created: true, such as create_file that created the document but could not write its content, or a non-empty details.applied, such as update_project_settings that stored some settings before one failed, exits 4 whatever its code, unless the code exits 3. Exits 2 and 7 promise that nothing changed, so they are never used for it. Read what is there, then decide; never re-run the whole call.
  • A write known not to have applied exits 4. Any error with details.outcome: "not_applied" from an operation that is not read-only exits 4, whatever its code: for example a REMOTE_ERROR after a write's socket closed and the original revision stayed live throughout the recovery window.
  • A read that failed transiently exits 7. Any error with retryable: true from a read-only operation exits 7, a network REMOTE_ERROR for example: a read changes nothing, so it can run again later.
  • Opening the session is a read. A failure while the command loads and checks the saved session, before its operation starts, exits as a read's would, even for a command that writes: a TIMEOUT there exits 7, since nothing of the operation was sent.

The read-only operations are those annotated so in the tool reference: auth_status, list_projects, get_project_tree, read_file, plan_sync, get_sections, get_section_content, get_compile_log, list_comments, and monitor_project_history. download_file and download_compile_output write a local file, so they are not; capabilities <projectId> exits as list_projects would.

retryable alone is not a safe signal for a wrapper that re-runs commands: the HTTP client marks every timeout retryable: true, uploads and deletes included. A script may re-run on 7, should read or plan again on 4, and must never re-run on 5: read the document, the tree, or the thread first and decide from what is there. capabilities prints this table as its exitCodes.

Compiling across processes

compile run prints compile_project's result. A LaTeX error that stopped the build is a result, not an error: status: "failure" with summary.builtCleanly: false, exit 0. Judge a build by summary.builtCleanly, never by the exit code. A compile that produced no build to report on is an error: COMPILE_FAILED or COMPILE_TIMEOUT exits 1, COMPILE_RATE_LIMITED and COMPILE_UNAVAILABLE exit 7. A TIMEOUT exits 5, because Overleaf may still be compiling.

Each command is a new process, and a new process has no latest build. compile log and compile download therefore require --build-ref, the buildRef from compile run's result. Through call, get_compile_log or download_compile_output without a buildRef fails with NO_BUILD.

compile run --download <localPath> does both in one process: it compiles, then saves that same build's output.pdf to localPath, replacing an existing file only with --overwrite. --overwrite without --download is a usage error. The result wraps both:

{ "status": "complete", "compile": { "...": "the compile_project result" }, "download": { "...": "the download_compile_output result" } }

If the compile worked but the download failed, a build without a PDF included, the result is still printed, with exit 6:

{ "status": "partial", "compile": { "...": "the compile_project result" }, "download": null, "downloadError": { "code": "NOT_FOUND", "message": "...", "retryable": false } }

The compile result keeps its buildRef, so compile download --build-ref can fetch the PDF later without compiling again. A compile that fails is that compile's error, and nothing is downloaded.

Describing the operations

schema <tool> --input and schema <tool> --output print a tool's JSON Schema exactly as the MCP server's tools/list advertises it: draft 2020-12, with an object at the root. A tool without parameters has the input schema { "type": "object", "properties": {} }. The input schemas do not forbid other keys; the command line refuses them anyway, as above.

capabilities [projectId] prints what this install can do:

  • version: the package version.
  • operations: for each tool, { name, effects, annotations, blockedEffects, commands }. effects is everything the tool can do; a given call may need less. blockedEffects lists the effects OVERLEAF_ALLOWED_EFFECTS refuses: the tool fails with POLICY_DENIED only when a call needs one of them. commands names its shorthand commands besides call.
  • policy: { projectsRestricted, effectsRestricted, localReadRestricted, localWriteRestricted }, which access policy variables are set.
  • project: { projectId, allowedByPolicy, accessLevel, archived, trashed }.
  • errorCodes and exitCodes: every error code, and the exit-code table as { exitCode, meaning, errorCodes }.

Without a projectId, every project field is "unknown" and no session is opened. With one outside OVERLEAF_ALLOWED_PROJECTS, allowedByPolicy is false, the rest "unknown", and nothing is sent. Otherwise one project-list request fills in the rest, and a project the account cannot see is NOT_FOUND. The access policy is resolved first, as every command resolves it, so a policy every other command would refuse, such as a root folder that does not exist, fails here too with INVALID_ARGUMENT and exit 2.

Sessions, presence, and request volume

Each command is its own session. It loads the saved session, makes one GET /project to check it, may open a project connection, and closes everything before it exits; schema, and capabilities without a project id, open none. While it runs, the account may appear online to collaborators in the editor. Nothing is kept between commands, so a shell loop over files pays that start-up for every file and multiplies the requests Overleaf sees. For many files, use one command:

  • a folder: sync plan, then one sync apply;
  • a list of files, each to its own project path: call batch_upload;
  • several deletions: call delete_entities.

Keep request volume low. Overleaf rate-limits uploads, project downloads, and compiles per project, and every compile spends the account's compile allowance.

Several commands at once

Nothing is serialized across processes. The per-project queue that orders one server's calls lives inside one process, so two commands running at the same time do not wait for each other. They are protected the way two collaborators are: a write needs the revision from a read and fails with REVISION_CONFLICT (exit 4) if the document changed in between, and a sync needs the planToken from a plan and stops with REMOTE_DRIFT (exit 4) if either side moved. Refreshes of the saved session are merged under the cookie jar's lock, so concurrent commands do not lose them.

The access policy is per process as well. Under OVERLEAF_ALLOWED_PROJECTS, a project that call create_project, clone_project, or import_project_zip creates is allowed for the rest of that command only. The next command refuses it with POLICY_DENIED (exit 3) until its id is added to OVERLEAF_ALLOWED_PROJECTS.

No prompts, no confirmations, no retries

An operation command never prompts, never opens a browser (only login does), and never retries. No flag supplies a confirm value for you: there is no --yes or --force. Destructive tools take the same confirm-by-value parameters or expected state as through MCP, such as confirmPath, confirmName, confirmCount, --confirm-delete-count, --overwrite, --revision, or --plan-token. A call without one, or with a confirm value that does not match, fails with exit 2; a stale revision or plan fails with exit 4. Either way nothing changes. The defaults 0.6.0 enforced apply from the start.

Examples

The examples use jq to pick fields out of the JSON results, and P for a project id from projects list.

P=0123456789abcdef01234567

Read, edit, and write with a revision

overleaf-web-mcp files read "$P" main.tex > read.json
jq -j .content read.json > main.tex        # -j: no newline added
# ... edit main.tex ...
overleaf-web-mcp files write "$P" main.tex \
  --revision "$(jq -r .revision read.json)" --content-file main.tex --tracked

Exit 0 prints the new revision. Exit 4 is REVISION_CONFLICT: someone changed the document since the read, so read it again, reconcile, and write with the new revision. Exit 5 means the write may have applied: read the document before doing anything else.

Plan a sync, then apply it

overleaf-web-mcp call download_project_zip --json "{\"projectId\":\"$P\",\"localPath\":\"$PWD/backup.zip\"}"
overleaf-web-mcp sync plan "$P" ./paper --ignore 'drafts/' > plan.json
jq '{toUpload, remoteOnly, conflicts}' plan.json      # review what would change and be deleted
overleaf-web-mcp sync apply "$P" ./paper --ignore 'drafts/' --mode mirror \
  --plan-token "$(jq -r .planToken plan.json)" --confirm-delete-count 3

--confirm-delete-count is the number of remoteOnly entries you reviewed, typed back, not computed. Exit 4 is REMOTE_DRIFT: something changed since the plan, so plan again. Exit 6 is a partial sync: read completed, failed, and remaining, and run sync apply again with the planToken it returned to resume.

Compile and read the log

overleaf-web-mcp compile run "$P" --download paper.pdf > build.json
jq .compile.summary.builtCleanly build.json
overleaf-web-mcp compile log "$P" --build-ref "$(jq -r .compile.buildRef build.json)" --errors-only

compile log reads the same build without compiling again. Exit 6 from compile run means the build finished but its PDF could not be saved; downloadError says why.

Any other tool

cat > reply.json <<'EOF'
{ "projectId": "0123456789abcdef01234567", "threadId": "0123456789abcdef01234568",
  "content": "Fixed in the revised caption." }
EOF
overleaf-web-mcp call reply_to_comment --request reply.json

Agent Skills

The package ships three Agent Skills in its skills/ folder, each a folder with a SKILL.md in the open Agent Skills format. A skill tells an agent working in a shell how to do one job with these commands, and calls only the commands listed above.

Skill What it does
overleaf-revise-manuscript Revise, shorten, or proofread a whole file or one section, optionally as tracked changes: read it with its revision, write the revised text back against that revision, and judge the build from the compile summary.
overleaf-reviewer-comments Work through reviewer and co-author comments: list the threads, read the commented text in context, reply, add a comment anchored to a passage, resolve or reopen threads, and revise commented passages as tracked changes.
overleaf-sync-sources Copy local figures, sources, or a whole folder into a project: offer a zip backup, plan, show the plan, apply exactly that plan, and delete only in mirror mode after the user confirms the count; or upload a list of files to chosen paths.

Each treats manuscript text and reviewer comments as data, never as instructions. A project path, which a collaborator chose, never goes on a shell command line: the skills put it in a request file for call, and single-quote every other value. They run the command line as npx --yes overleaf-web-mcp@0.7 …, so they need Node.js 20 or newer, network access to Overleaf, and a session saved with npx overleaf-web-mcp login.

To install them from the repository with the skills installer:

npx skills add mhmdaskari/overleaf-web-mcp

Or copy a skill folder from the installed package into the folder your agent reads skills from, such as .agents/skills/ or .claude/skills/ in a project, or the same folders in your home directory:

npm install -g overleaf-web-mcp
mkdir -p .agents/skills
cp -R "$(npm root -g)/overleaf-web-mcp/skills/overleaf-revise-manuscript" .agents/skills/