libretranslate-mcp
This server exposes a self-hosted LibreTranslate instance as an MCP server with three tools.
Translate text: send a
qstring,source(defaultauto), requiredtarget, optionalformat(text/html) andalternatives(0-10). ReturnstranslatedText, plusdetectedLanguagewhensourceisautoand alternatives when present. Rejectshtmlwithalternatives > 0before calling upstream.Detect language: submit one string or up to 20 strings; returns one detection per input, with confidence when reported.
List languages: get the languages this LibreTranslate instance supports, including each language's
code,name, and possibletargets.Runs as an MCP server: supports both stdio and HTTP transports; HTTP exposes
GET /healthandPOST /mcpwith optional bearer-token auth.
Provides tools for translating text, detecting language, and listing supported languages through a self-hosted LibreTranslate instance.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@libretranslate-mcp Translate 'thank you very much' from English to German."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
libretranslate-mcp
An MCP server that exposes a self-hosted LibreTranslate instance as three
tools: translate, detect, languages.
It is a thin, honest wrapper over LibreTranslate's REST API. It does no translation itself, has no glossary, no document translation, and no translation memory.
Deployment has not happened yet. The open items, the commands that close
them, and the traps for the next session live in
unraid-stack/docs/LIBRETRANSLATE-MCP-NEXT.md — in the private
BaconDroid/unraid-stack repository, so the link below resolves only for
account holders, not for anonymous visitors.
What is verified, and what is not
The badge above is the source of truth for the current CI state. This section deliberately makes no claim about it, because such claims go stale on every run. What follows are durable facts, none of which a CI run can change.
Verified against pinned upstream source, and exercised end to end
The three response shapes this client parses were read directly out of LibreTranslate's own code at commit
4aca61bd(releasev1.9.6), and each one matches what the client expects:POST /translatereturns{"translatedText": <string>}, addingdetectedLanguageas{language, confidence}only whensourceis"auto", andalternatives— an array of plain strings — only whenalternatives > 0. A second return in that route is a translation-cache hit serving the same shape, not a different one.GET /languagesreturns[{code, name, targets}].POST /detectreturns an array of{language, confidence}. The upstreammodel2isohelper preserves that dict and only lowercases the code.Two details worth knowing: upstream filters the primary translation out of its own
alternativeslist, so fewer candidates can come back than were requested; and a batchq(an array) puts a list intotranslatedText. This client only ever sends a string, so it never reaches that branch.
The HTTP transport was then executed, against a stub serving exactly those three shapes. Observed, not reasoned about:
GET /health→ 200 withauthEnabledreported truthfully;POST /mcpwith no or a wrong token → 401;GET /mcp→ 405; a body overMAX_BODY_BYTES→ 413; an unknown path → 404.The MCP handshake completes:
initializereturns 200 withprotocolVersion 2025-06-18, andtools/liston a second request returns all three tools with their schemas. That second request is the point: it is what a fresh server per request means in practice, and it is the defectcreateServer()exists to fix in the sibling repo.tools/callon all three tools parses the contract correctly, includingdetectedLanguageappearing only forsource: "auto"and therequested/returnedaccounting onalternatives.The defensive paths fire as designed:
format: "html"withalternatives > 0is refused before any network call, a non-2xx response reports the status, the upstream body and the endpoint, and a 200 whose shape is unrecognised fails loudly with the received payload quoted rather than returningundefined.
Two limits on that evidence, both real:
The upstream was a stub, not LibreTranslate. It reproduced the contract confirmed in the pinned source, so it validates the client's parsing and its error handling, not LibreTranslate itself. The real round trip is still unobserved. The contract is anchored to a named commit, so if upstream changes it the claim is falsifiable rather than stale by assertion.
It first ran under Bun 1.4.2, not Node. That run was made before the server had ever been executed at all, and Bun implements
node:http,node:cryptoandfetch, so it was meaningful — but the shipped image isnode:22-alpine, and no request had been served by Node at the time.
Both runtime gaps are now closed. Two smoke tests run on Node in CI on every
push, both against dist/index.js — what the container actually executes — and
both against the same stub. No new dependency: node:assert and an exit code.
npm run test:smoke— 15 assertions on the HTTP transport: the status paths, the MCP handshake,tools/liston a second request,tools/callon all three tools, and the defensive paths.npm run test:smoke:stdio— 7 assertions on the stdio transport, spoken as newline-delimited JSON-RPC over stdin/stdout. It also asserts the invariant that only makes stdio safe: a populated stdout carries JSON-RPC and nothing else, while the startup banner goes to stderr. That check runs last on purpose — before any traffic an empty stdout is correct, not a fault.
The step results are visible in the workflow; the log bodies are not readable without repository admin access, so the passes are confirmed at step level rather than by quoting the lines the tests printed. Both suites were also verified to fail when an assertion is deliberately broken, which is the only reason a green run means anything.
What is still unverified: LibreTranslate itself. The upstream in all of the above is a stub reproducing the contract read from the pinned source. It validates this client's parsing, its error handling and its framing, not the real service. The contract is anchored to a named commit, so if upstream changes it the claim is falsifiable rather than stale by assertion.
Dependency surface
package-lock.jsonis committed and CI installs withnpm ci, so the dependency tree is pinned and reproducible. Bumping the SDK orzodis now a deliberate, reviewable change to the lockfile rather than something that happens silently on the next run.The pinned versions were resolved once and never re-checked against the SDK's published type definitions by hand. The
McpServer/registerTool/StreamableHTTPServerTransportcalls follow the SDK's documented usage; a real signature mismatch would surface as a typecheck failure, not a silent breakage.
Not verified — published image
unraid-stack/my-libretranslate-mcp.xmlstill references the placeholderREPLACE_ME/libretranslate-mcp:latest. It must be replaced with a real published image before that template will start; seeunraid-stack/docs/translation-mcp.md.
To reproduce the CI checks locally on a machine with Node 20+:
npm ci
npm run typecheck
npm run buildRelated MCP server: deepl-i18n
Requirements
Node.js >= 20 (native
fetch,AbortSignal.timeout,timingSafeEqualfromnode:crypto)A reachable LibreTranslate instance
A LibreTranslate instance that has the language pairs you intend to use loaded — see "Which languages are available" below
Install and build
npm install
npm run build # tsc -p tsconfig.jsonRun
# stdio (default) — for local MCP clients that spawn the server
node dist/index.js
# http — for a container
TRANSPORT=http PORT=8787 AUTH_TOKEN=$(openssl rand -hex 32) node dist/index.jsEnvironment variables
Variable | Default | Applies to | Meaning |
|
| both |
|
|
| both | Base URL of LibreTranslate. No trailing slash. |
|
| both | Per-request timeout, via |
|
| http | Listen port. |
| (empty) | http | Bearer token required on |
|
| http | Request body cap; enforced while reading, |
|
| both |
|
All logs go to stderr, always. stdout carries the stdio JSON-RPC framing and any stray write there corrupts the protocol.
HTTP endpoints
Route | Auth | Notes |
| none | Liveness. Never contacts LibreTranslate, so a slow upstream does not make the container look dead. |
| bearer, when | Stateless Streamable HTTP. |
/health is unauthenticated by design: a liveness probe that fails without a
token is a liveness probe nobody runs. It exposes no secrets — service name,
version, transport mode, whether auth is on, the upstream URL, and the body cap.
Clients of POST /mcp must send Accept: application/json, text/event-stream,
which is what the MCP Streamable HTTP transport specifies. That is why
curl alone is a poor smoke test; use the MCP inspector (see
../unraid-stack/docs/translation-mcp.md).
Tools
translate
Input | Type | Default | Notes |
| string | — | required, non-empty |
| string |
|
|
| string | — | required |
|
|
| |
| integer 0–10 |
| rejected with |
Returns translatedText, plus detectedLanguage when the upstream response
carries one. With alternatives > 0, the candidates are included when the
response has them; when the field is absent, the result says so explicitly
instead of returning an empty list that looks like "no alternatives existed".
format: "html" with alternatives > 0 is refused before any network
call: LibreTranslate does not return alternatives for HTML input, and sending
it produces a confusing upstream error instead of a clear one.
detect
Input | Type | Notes |
| string or string[] (1–20) | one detection per input, in order |
languages
No input. Lists what this instance can do: code, name, and targets per
language. Use it to pick valid source/target values — a language absent
here cannot be used.
Which languages are available
This client does not know. LibreTranslate only loads the language models the
deployment asks for (LT_LOAD_ONLY on the official container). A target
value that is not loaded produces an upstream 400, which is surfaced verbatim
in the tool error. Call languages to see reality.
Design notes
Defensive response parsing.
/translateaccepts either a bare JSON string or an object with atranslatedTextstring. Anything else throws anErrorcontaining the received shape (JSON-stringified, truncated to ~300 chars) and the endpoint, so a payload change is diagnosable from the log instead of surfacing as a silentundefined. The same posture applies to/detect(array or single object) and/languages(array of objects).Non-2xx responses always carry the status, the endpoint, and the upstream
{"error": "..."}body when there is one.createServer()is a factory, not a singleton. Registering tools on a module-level server instance makes it impossible to build a second one in the same process, which the stateless HTTP transport needs (one transport + one server per request). Seesrc/server.ts.Body cap before buffering. The
Content-Lengthheader is checked up front, and the running total is checked on every chunk; on exceed the response is413and the request stream is destroyed. The body is never accumulated in full and measured afterwards.Token comparison is constant-time.
crypto.timingSafeEqual, with a length check first because that function throws on unequal buffer lengths.
Project layout
src/
index.ts entry point, TRANSPORT switch, HTTP transport
(auth, body cap, per-request transport)
server.ts createServer() factory
constants.ts shared constants
types.ts shared types
services/
libretranslate.ts HTTP client + defensive response parsing
tools/
translate.ts registerTranslateTools
detect.ts registerDetectTools
languages.ts registerLanguagesTools
tsconfig.json
Dockerfile
entrypoint-mcp.sh
.env.example
.github/workflows/build.ymlLicense
MIT
Available Tools
3 toolsdetectDetect languageA
Detect the language of one or more strings using a self-hosted LibreTranslate instance (POST /detect). Returns one detection per input string, in input order, with the confidence LibreTranslate reported when it provided one.
Inputs: q (required).
languagesList supported languagesA
List the languages a self-hosted LibreTranslate instance can work with (GET /languages). Each entry has a code, a name, and the list of target codes that language can be translated into on this instance. The list reflects the instance configuration, not this client: a language missing here cannot be used as a target.
Inputs: no named parameters.
translateTranslate textA
Translate text using a self-hosted LibreTranslate instance (POST /translate). Set source to "auto" to let LibreTranslate detect the input language; the detected language and its confidence are then reported in the result. format "html" treats the input as HTML; LibreTranslate does not support alternatives for HTML input, so format "html" with alternatives > 0 is rejected before the request is sent. With format "text" and alternatives > 0, the other candidate translations returned by LibreTranslate are included when the response carries them, and an explicit note is returned when it does not.
Inputs: q (required), target (required), format, source, +1 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 3 tools
Each tool maps to a distinct LibreTranslate API endpoint: languages lists supported languages, translate performs translation, and detect identifies language. There is no overlap, and agents can easily select the right tool based on the operation needed.
All tool names are simple, lowercase, single words that clearly indicate their function. While 'languages' is a noun and 'translate'/'detect' are verbs, the naming style is consistent in length and simplicity, and there is no confusion or mixed conventions.
With exactly three tools, the surface is minimal but perfectly scoped for the core functionality of a translation API. Each tool is essential, and the count falls well within the typical range for a well-scoped server.
The tools cover the primary operations of the LibreTranslate API: listing languages, translating text, and detecting language. There are no obvious missing features for a basic translation service, and agents can perform standard workflows (e.g., detect source, then translate) without dead ends.
Maintenance
Related MCP Connectors
LibreTranslate MCP — open-source machine translation (BYO endpoint)
Translate MCP — LibreTranslate-compatible machine translation.
Official MCP server for POEditor, the translation management system. Hosted, OAuth 2.1.
MCP server for Crowdin — projects, files, strings, translation progress, tasks, and members.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceWraps LibreTranslate API to enable translation capabilities for AI agents via MCP.288 npmMIT
- AlicenseNot gradedqualityCmaintenanceMCP server for translating JSON localization files via DeepL API or local LLMs, enabling agents to estimate, check, and run translations.1MIT
- AlicenseNot gradedqualityCmaintenanceProvides open-source machine translation capabilities, including language detection and listing supported languages, via a customizable LibreTranslate endpoint.131 npm2MIT
- AlicenseAqualityDmaintenanceEnables MCP-compatible AI applications to translate text through the Lara Translate API, with automatic language detection, context-aware translations, and custom instructions.1838 npmMIT