subtitle-translation-mcp
by Majrooo
README.md
# subtitle-translation-mcp
[](https://github.com/Majrooo/subtitle-translation-mcp/actions/workflows/ci.yml) [](https://github.com/Majrooo/subtitle-translation-mcp/releases) [](https://github.com/Majrooo/subtitle-translation-mcp/blob/main/LICENSE) [](https://nodejs.org) [](https://www.typescriptlang.org)
Offline MCP server for subtitle **translation** with a persistent translation session, a translation-memory glossary, and modular format adapters. Built for LLM agents (e.g. Cline) — the server structures and persists, the agent translates.
- **Timing is never touched.** The source timing/order is stored once at parse time; `translate_batch` sends only `{ index, text }`, so cues can never drift or merge.
- **Persistent session.** Interrupted translation? Resume exactly where you left off (`translation_status` → `missingRanges`).
- **Glossary (translation memory).** Names and fixed terms are translated consistently across movies and series.
- **Whole-film context.** `subtitle_overview` gives the agent the full plain text first, so names/terminology stay consistent.
- **Encoding-safe.** Auto-detects UTF-8 / Windows-1250 / ISO-8859-2 / Windows-1251 / ISO-8859-1 — mojibake is never silently translated.
> **Languages:** [English](README.md) · [Slovenčina](README.sk.md)
## Layout
```
<server dir>/
├── Subtitles\ # SUBTITLES_ROOT — movie folders (Subtitles/<movie>/<file>)
│ ├── Movie A\
│ │ ├── movie-a.srt
│ │ └── glossary.json # movie/series-scoped glossary
│ └── Movie B\
│ └── movie-b.srt
└── data\ # SRT_DATA_ROOT — session.json + global glossary.json (+ .bak)
```
Override the roots with the `SUBTITLES_ROOT` and `SRT_DATA_ROOT` env vars.
## Quick start
Requires **Node.js 20+** (tested on 20/22).
```bash
npm install
npm run build # tsc → build/
npm test # vitest run
```
MCP config (VS Code / Cline):
```json
{
"mcpServers": {
"subtitle-translation-mcp": {
"command": "node",
"args": ["<path-to-repo>/build/index.js"],
"env": {
"SUBTITLES_ROOT": "<path-to-repo>/Subtitles",
"SRT_DATA_ROOT": "<path-to-repo>/data"
}
}
}
}
```
## Tools (25)
| Tool | Purpose |
|---|---|
| `list_movies` | List movie folders and their subtitle files |
| `list_formats` | List supported subtitle formats (SRT, MicroDVD `.sub`, ASS `.ass`) |
| `info_subtitles` | Block count, first/last timecodes, validation issues; reports detected `encoding` + `detectionConfidence` |
| `parse_subtitles` | Return a page of blocks (`startBlock`/`blockCount`); stores the full source (timing/order) into the session on first call |
| `subtitle_overview` | Whole-movie plain text (no indexes/timecodes); `outFile` writes a `.txt`, `maxLines` caps output |
| `search_cues` | Find cues by text/regex, with optional surrounding context — check name/term consistency |
| `replace_cues` | Replace text/regex across cues (literal or regex); `outFile` writes the result; timing untouched |
| `translate_batch` | Store translated blocks — send only `{ index, text }`; reports unknown/empty indexes |
| `build_subtitles` | Assemble the full file from the session; `outFile` write / format conversion, `includeContent`/`preview` |
| `translation_status` | Session status: total/stored/remaining + `missingRanges` (resume after interruption) |
| `clear_translation` | Clear the persistent session for a file |
| `glossary_get` | Read the translation memory (global + movie-scoped entries; optional `srcLang` filter) |
| `glossary_add` | Add/update a term (`term` → `translation`, `srcLang`, `scope`) — write only after explicit user consent |
| `validate_subtitles` | Structural validation: duplicates, inverted timecodes, overlaps, empty cues |
| `write_subtitles` | Write subtitle content to a file inside the root (UTF-8, `overwrite` guard) |
| `shift_timecodes` | Shift all timecodes by ±ms |
| `retime_timecodes` | Rescale and/or shift timecodes: `t' = round(scale·t + offset)` — e.g. fps conversion (25 → 23.976) |
| `reindex_subtitles` | Re-number cues sequentially |
| `clean_sound_cues` | Remove sound-effect cues (e.g. `[krik]`); `mode` "cues+lines" (default) also strips `[...]` from mixed lines; writes to a new `outFile`, `dryRun` preview |
| `generate_release_note` | Write the three-version `README.txt` release note for a finished translation |
| `estimate_retime` | Suggest scale+offset to align `file` timing with `refFile` (language-agnostic, robust regression) |
| `convert_format` | Standing format conversion without a session (target format by `outFile` extension) |
| `check_line_lengths` | Flag cues with lines longer than `maxChars` (rule ~37–42 chars/line) |
| `fix_overlaps` | Push overlapping cues past the previous end (+ optional `gap`) |
| `find_untranslated` | Find cues with non-Latin script (CJK/Cyrillic) or identical to the source |
## Translation workflow (EN→SK example)
1. `info_subtitles` → sanity check + encoding.
2. `subtitle_overview` → read the whole film (identify characters/terminology).
3. `glossary_get` → load the translation memory; propose new terms, start only after approval.
4. `parse_subtitles` in batches (e.g. 100 blocks) — stores full source.
5. Agent translates and sends **only** `{ index, text }` via `translate_batch`.
6. Repeat until `remaining: 0` (track with `translation_status`).
7. `build_subtitles` with `outFile` → merged result.
8. `validate_subtitles` on the result.
9. `generate_release_note` → three-version `README.txt` in the movie folder.
## Agent translation rules
The server structures and persists, but the actual translation is done by an **LLM agent** (e.g. Cline). Quality depends on the agent following a strict rule set: review the whole film first, no mojibake, correct audio-track language, glossary priority, informal T-form, no Czechisms, name transliteration per Slovak rules, ~37–42 chars per line, source-language selection (CZ > Slavic > DE/FR > EN for Western films; original ZH/JA/KO > Slavic > EN for Asian films).
The quality also depends on the **LLM model itself**: it should handle at least **256k context** (above 512k recommended — a single full-movie overview can be large), and it should be linguistically strong in Slovak. The project is developed and tested with **DeepSeek-V4-Flash**.
The public version is in [`docs/translation-rules.en.md`](docs/translation-rules.en.md) (English) and [`docs/translation-rules.sk.md`](docs/translation-rules.sk.md) (Slovak). The private working copy lives in `.clinerules/` and is not part of this repository.
## Docs
- [`docs/capabilities.md`](docs/capabilities.md) (EN) / [`docs/capabilities.sk.md`](docs/capabilities.sk.md) (SK) — what you can do with this tool (use cases & ideas).
- [`docs/faq.md`](docs/faq.md) (EN) / [`docs/faq.sk.md`](docs/faq.sk.md) (SK) — troubleshooting and setup.
- [`docs/tools.md`](docs/tools.md) — the full tool list.
- [`docs/usage.md`](docs/usage.md) — annotated example tool calls.
- [`docs/source-language-preference.en.md`](docs/source-language-preference.en.md) (EN) / [`docs/source-language-preference.sk.md`](docs/source-language-preference.sk.md) (SK) — how to pick the best source track (e.g. content from CZ, timing from EN, names from ZH).
- Translations of the docs into other **Slavic languages** (PL, SR, BG, …) are welcome as pull requests.
## Adding a new format (modular)
1. Create `src/formats/<name>.ts` exporting an adapter implementing `SubtitleFormat`.
2. Register it in `src/core/formats.ts`:
```ts
import { vttAdapter } from "../formats/vtt.js";
export const ADAPTERS: SubtitleFormat[] = [srtAdapter, microDvdAdapter, assAdapter, vttAdapter];
```
No other code changes — all tools are format-agnostic.
## Safety
- All `movie`/`file`/`outFile` paths are validated to stay inside the configured root; inner separators are rejected (flat `Subtitles/<movie>/<file>` layout).
- No shell execution — everything uses Node.js `fs` APIs.
- Writes happen only via the write tools (with the `overwrite` guard on `write_subtitles`).
## Development
```bash
npm install
npm run build # tsc
npm test # vitest run (170 tests)
```
## License
MIT — see [LICENSE](LICENSE).This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues