TypeScript SDK¶
overleaf-web-mcp/core is the engine behind the MCP server, as a library. createOverleafClient
returns one typed method per tool, with the same checks, safety defaults, results, and error
codes as the MCP server and the command line. It never loads the MCP SDK.
Install¶
The package is ESM only and needs Node.js 20 or newer. The client uses the session login saves
and reads the same environment variables as the server; see configuration.
A minimal program¶
import { createOverleafClient, McpError } from 'overleaf-web-mcp/core'
const projectId = '0123456789abcdef01234567'
const client = await createOverleafClient()
try {
const file = await client.readFile({ projectId, filePath: 'main.tex' })
const content = file.content.replaceAll('teh ', 'the ')
const written = await client.writeFile({ projectId, filePath: 'main.tex', revision: file.revision, content })
console.log(written.revision)
} catch (error) {
if (error instanceof McpError && error.code === 'REVISION_CONFLICT') {
console.error('main.tex changed since it was read; read it again and reconcile.')
} else {
throw error
}
} finally {
await client.close()
}
createOverleafClient() opens the saved session, as the server does at start-up, and rejects with
AUTH_EXPIRED when there is none or it has expired; any failure to open it rejects with
McpError. Close the client in finally: it caches
project connections, which keep the process alive, and can show the account online to
collaborators, until it is closed.
Methods¶
The client has one method per tool, named in camelCase: read_file is readFile,
sync_directory is syncDirectory, and auth_status is authStatus(). A tool whose parameters
are all optional, such as listProjects(), can be called with no argument.
It also has close(). Each method takes the tool's input, the same object an MCP client sends as
arguments and the tool reference documents, and an optional OperationContext:
readFile(input: OperationInput<'read_file'>, context?: OperationContext): Promise<OperationOutput<'read_file'>>
close() waits for the calls still running, then closes the project connections, so a write in
flight is never cut off. From the moment it is called, every method rejects with
INVALID_ARGUMENT; create a new client. Calling close() again waits for the same close. Do not
await close() inside an onProgress callback of a call that is still running: it would wait for
that call, which waits for the callback.
Input is strict¶
As on the command line, the input is checked against the tool's input schema before anything is
sent. A key the schema does not list, at any depth, rejects with INVALID_ARGUMENT and
details.unknownKeys; a value the schema refuses rejects with INVALID_ARGUMENT and
details.issues, each { path, message }. Omitted fields take the documented defaults, as
through MCP. The MCP server drops unknown keys instead, so a misspelled writeMode there falls
back to untracked; see the command line. TypeScript catches most such
mistakes at compile time; the check covers JavaScript callers and input built at run time.
Results¶
A method resolves with the tool's result: the same object the MCP server returns as
structuredContent and the command line prints, a plain JSON value typed as
OperationOutput<'tool_name'> from the tool's output schema.
The results of read-only tools are also checked against that schema at run time. One that does not
match, because Overleaf answered in a shape this release does not understand, rejects with
PROTOCOL_UNSUPPORTED and details: { kind: "response_shape", paths }. The result of a tool
that changes something is typed but not checked, because an error reported after a write applied
would invite resubmitting it.
Errors¶
Every method rejects with an McpError. It has code, one of ERROR_CODES; message;
retryable; and optional details. toJSON() returns { code, message, retryable, details? },
exactly what the command line reports, and what the MCP server reports for any failure of the
operation itself. Input the schema refuses is the exception: the MCP SDK checks it before the
server runs the tool and answers with its own plain-text error, with no code or details, where the
client and the command line report INVALID_ARGUMENT with details.issues, or
details.unknownKeys for a key the schema does not list. The
safety model lists the codes, and the command line's
exit codes are a good rule for deciding what to do next.
The client never retries. retryable: true is advisory, and a TIMEOUT from a write carries it
too: when details.outcome is unknown, or the code is OUTCOME_UNKNOWN, read the document or
the tree before doing anything else, and never call the write again blindly.
Progress, cancellation, and diagnostics¶
OperationContext is what an interface hands an operation besides its input:
| Field | Meaning |
|---|---|
onProgress(progress, total, message) |
Called as plan_sync, sync_directory, batch_upload, and delete_entities advance. Messages hold counts only. Its failures are ignored. |
signal |
An AbortSignal. Cancelling it stops waiting; it never aborts a write already submitted. No operation in this release observes it yet. |
onDiagnostic({ tool, code }) |
Called when an operation fails, with its error code, or relied on a deprecated default, with DEPRECATED. It never carries a message, path, or content. |
Stable surface¶
These exports follow the project's versioning: until 1.0.0 a change can come in a minor release, and every change is listed in the changelog.
| Export | What it is |
|---|---|
createOverleafClient |
createOverleafClient(options?) opens the session and returns an OverleafClient. options.config defaults to readConfig(). |
OverleafClient |
The client type: one method per tool, plus close(). |
OverleafClientOptions |
createOverleafClient's options. Only config is public. |
OperationMethod |
The type of one client method. |
CamelCase |
The method name for a tool name, as a type: CamelCase<'read_file'> is 'readFile'. |
createOverleafService |
createOverleafService(runtime) returns one function per tool under its snake_case name, taking already-parsed input. The client is built on it. |
OverleafService |
The type createOverleafService returns. |
OverleafRuntime |
The session and engine. Only OverleafRuntime.create(config), which opens the session, and runtime.close() are stable. |
readConfig |
readConfig(env?) reads the configuration from process.env, or the object given. |
AppConfig |
The configuration readConfig returns. |
McpError |
The error every method rejects with. |
McpErrorCode, ErrorCode |
One of ERROR_CODES. |
ERROR_CODES |
Every error code, through every interface. |
OPERATIONS |
The contract of every tool: description, input and output schemas, annotations, and effects. |
OPERATION_NAMES |
The tool names, in registration order. |
operationContract |
operationContract(name) returns one entry of OPERATIONS. |
OperationName |
One of OPERATION_NAMES. |
OperationInput |
OperationInput<'tool_name'>, what a tool accepts; defaulted fields may be left out. |
OperationOutput |
OperationOutput<'tool_name'>, what a tool resolves with. |
OperationContext |
The optional second argument of every method. |
ProgressReporter |
The type of onProgress. |
OperationDiagnostic |
What onDiagnostic receives. |
OperationContract, OperationAnnotations |
The shape of one OPERATIONS entry and its MCP annotations. |
EFFECTS, READ_EFFECTS, Effect |
The effects an operation can have, as the access policy names them. |
WriteMode |
"untracked" or "tracked". |
SERVER_VERSION |
The package version. |
Everything else the module exports is internal and can change in any release: the other fields
and methods of OverleafRuntime (http, documents, connections, policy, and the domain
APIs), RuntimeDependencies with its fetcher and connectionFactory, the dependencies option
of createOverleafClient, and the OverleafServiceRuntime and OperationHandler types.
Using the service directly¶
createOverleafService is the layer the MCP server, the command line, and the client share. It
expects input already parsed against the tool's schema, as the MCP SDK delivers it: it neither
refuses unknown keys nor applies the schema's defaults itself. Prefer the client unless you parse
input yourself.
import { createOverleafService, OverleafRuntime, readConfig } from 'overleaf-web-mcp/core'
const runtime = await OverleafRuntime.create(readConfig())
try {
const service = createOverleafService(runtime)
const tree = await service.get_project_tree({ projectId: '0123456789abcdef01234567' })
console.log(tree.rootDocPath)
} finally {
await runtime.close()
}
Every domain method checks the access policy before any request, so code that reaches past the service is held to the same rules.