estonian-mcp
# estonian-mcp
> Claude is quite bad at Estonian, so this MCP is here to fix that. Give it a shot.
[](https://github.com/silly-geese/estonian-mcp/actions/workflows/ci.yml)
[](https://smithery.ai/servers/silly-geese/estonian-mcp)
[](LICENSE)
[](pyproject.toml)
[](https://modelcontextprotocol.io)
A small **Model Context Protocol** server that exposes
[EstNLTK](https://github.com/estnltk/estnltk), the Estonian NLP toolkit,
as tools any LLM client can call in real time, backed by EKI's orthography
rules (Reeglid) and public-domain Riigi Teataja legislation. Hand it
Estonian text, get back correct lemmas, morphology, POS tags, spell-check +
suggestions, syllables, named entities, WordNet synonyms, fastText-based
related words, a register hint, orthography/grammar checks, kantseliit and
terminology-consistency checks for reports and academic prose, and, for
legal texts, legalese simplification and canonical legal-usage lookups.
**One-click install** from Anthropic's official Connectors Directory, or
self-host. See below.
If your AI agent has to draft, edit, or proofread Estonian, this wires
in ground truth so it stops guessing on the mechanical layer
(spelling, case forms, conjugation) and gives it real Estonian
synonyms instead of inventing them.
It also covers the **editorial** layer, where a word can be correctly
spelled, morphologically valid and still wrong: `check_officialese` for
bureaucratic Estonian in reports and academic prose, `check_term_consistency`
for a document that names one thing three ways, and `synonyms` read as a
word-fit check — its glosses carry domain constraints (`korpus` is
specifically "kirjaliku või suulise teksti elektrooniline kogu", so a set
of images is not one, however natural it sounds in ML jargon).
> **Benchmark:** on TalTech's [`inflection_et`](https://huggingface.co/datasets/TalTechNLP/inflection_et)
> gold dataset (a noun-phrase inflection benchmark; Lillepalu & Alumäe,
> [arXiv:2510.21193](https://arxiv.org/abs/2510.21193v2)), our morphology
> engine scores **99.1% first-candidate / 99.1% any-candidate** over
> 1,400 items (first-candidate is 96.6% without the optional fastText
> model, which is what ranks a lemma's inflection types; any-candidate is
> 99.1% either way). Every one of the 13 residual misses is a gold row that
> contradicts EKI, so EKI-adjudicated the score is **100% / 100%**; the
> disputed rows are listed with their citations in
> [`data/inflection_et_eki_disputes.json`](data/inflection_et_eki_disputes.json).
> `uv run python scripts/apply_eki_corrections.py` rebuilds the dataset
> locally with those 13 rows corrected. The corrections live here; the
> data does not, because the dataset carries no licence and re-hosting it
> is not ours to do.
> Reproduce: `uv run python scripts/eval_inflection.py`.
> (We're a tool server, not a rankable LLM, so this scores our tools
> against published gold data.)
> **Second benchmark, wider and from real text:** `inflection_et` covers
> four cases, so ten were never measured. Against Pert Lomp's
> [käänamiskorpus](https://github.com/pertlomp/qwen38-et) (11,011
> single-word rows over **all fourteen cases**, drawn from Riigikogu
> stenographs and ERR news and frequency-weighted, CC-BY-SA-4.0), the
> engine scores **99.3% first-candidate / 99.9% any-candidate**.
> The gap between those two is the interesting part: Estonian forms most
> plural oblique cases two ways, and leading with the wrong one cost 11
> points until 0.5.10 ordered a slot's variants by the paradigm's own
> genitive stem (`raamatutele` before the literary `raamatuile`).
> Reproduce:
> `uv run python scripts/eval_kaanamiskorpus.py`. The data is downloaded
> at run time, never vendored here, and the server never touches it.
**Three ways to use it:**
1. 👉 **One-click from Anthropic's Connectors Directory**, the easiest
path, no terminal, no install, no URL to paste. See
[Get started in 30 seconds](#-get-started-in-30-seconds-no-install) below.
2. **One-click on Smithery**, install from the
[estonian-mcp listing](https://smithery.ai/servers/silly-geese/estonian-mcp).
3. **Self-host**, clone, run locally as stdio, or deploy your own
container to Fly.io / any host. See [Self-host (advanced)](#self-host-advanced).
## What it does
| Tool | What it does |
| --- | --- |
| `tokenize(text)` | Split text into sentences and words |
| `analyze_morphology(text)` | Lemma, POS, form and its Estonian name, root, ending, clitic, compound parts, ambiguity count, and usage flags (archaic / foreign / interjection / abbreviation / proper-noun) per word |
| `paradigm(word)` | Full Vabamorf-generated inflection paradigm, 14 cases × 2 numbers for nominals (including ordinals, comparatives and superlatives), plus the short illative where a word has one (`majja` beside `majasse`), ~39 verb forms including the umbisikuline tegumood (`kasutatakse`, `kasutati`), with Estonian labels per form. A lemma with several inflection types (`kott` → `koti` or `kota`, two different words) returns one consistent table per type, corpus-ranked, rather than a merged one; pass an inflected form (`koti`) to select the type you mean |
| `lemmatize(text)` | Just the dictionary form per word |
| `pos_tag(text)` | Just the part-of-speech tag per word |
| `spell_check(text)` | Spelling check + correction suggestions |
| `syllabify(word)` | Syllables with quantity + accent |
| `named_entities(text)` | People / places / organisations |
| `synonyms(word)` | Synsets from Estonian WordNet, synonymous lemmas + definition + examples per word sense |
| `find_related_words(word)` | Top-N semantically nearby words via fastText embeddings (semantically related, not always synonymous) |
| `classify_register(text)` | Coarse formal/colloquial register hint with matched markers, consistency flag for register-mixed text, plus structural signals (umbisikuline tegumood ratio, noun density) so dense officialese no longer scores "neutral" |
| `check_style(text)` | Style metrics, lemma-aware repetition, umbisikuline-tegumood ratio, sentence-length variance, hedging-word density |
| `check_officialese(text)` | Kantseliit check for **non-legal** prose (reports, academic, business), where `check_legalese` stays silent. Nominalisation density (`hindamine` → `hindama`), impersonal-voice ratio, clause stacking (`mille käigus … ning …`), Estonian-calibrated sentence length, and admin filler (`omab` → `on`, `viidi läbi` → `tehti`, `mudeli poolt loodud` → `mudeli loodud`) |
| `check_term_consistency(text)` | One referent, one term. Flags a document that calls the same thing `andmestik` on page 1 and `teadusandmestik` on page 2, via shared compound head or shared Estonian WordNet synset, with per-variant counts so you can standardise on the dominant one |
| `check_redundancy(text)` | Pleonasm check, flags semantic doubling like `samuti ka` (also+also), `kõige optimaalsem` (most+optimal), and fixed redundant phrases |
| `check_object_case(text)` | Käändeõpetus, flags direct-object case errors under negation and after partitive-only verbs (armastama, vihkama, vajama, …) |
| `check_abbreviation_hyphenation(text)` | Lühendiortograafia, flags abbreviations with case endings missing the EKI-mandated hyphen (`MCPst` → `MCP-st`, `OÜle` → `OÜ-le`) |
| `check_compound_familiarity(text)` | Calque-risk diagnostic, for each compound noun, returns top fastText neighbours and flags compounds with weak similarity (`mõtteliin`-style translationese, e.g. literal English "train of thought" → real Estonian is `mõttekäik`) for second-look review |
| `check_capitalization(text)` | Algustäheortograafia check, flags wrongly capitalized weekdays, months, nationalities, and language/culture adjectives per EKI's Reeglid |
| `check_compounds(text)` | Liitsõnaõigekiri, flags common AI-generated splits of words that should be a single compound (`kooli maja` → `koolimaja`) |
| `check_punctuation(text)` | Kirjavahemärgid, flags missing commas before subordinating conjunctions (`et`, `sest`, `kuna`, `kuid`, `vaid`, `nagu`, …) |
| `check_hyphenation(word)` | Poolitamine, safe line-break positions for an Estonian word, syllable-boundary based with no-orphan-edge rule |
| `check_numbers(text)` | Numbrite õigekirjutus, flags decimal separators (`3.14` → `3,14`) and thousands separators (`1,000,000` → `1 000 000`) |
| `check_legalese(text)` | Legal plain-language aid, flags archaic `kantseliit` filler (`käesolev` → `see`, `juhul kui` → `kui`) and over-long sentences to simplify, while listing the **terms of art** that must be preserved so simplification doesn't change legal meaning |
| `check_defined_terms(text)` | Long-document structure, maps terms defined with `(edaspidi «X»)`, counts their usage, lists `§` / `lõige` / `punkt` cross-references, and flags defined-but-unused or doubly-defined terms (cap raised to 500k chars) |
| `common_legal_usage(word)` | Canonical legal collocations from an offline corpus index, how often a term occurs in legislation and the words most often seen before/after it (`hagi` → `esitama hagi`, `kohustus` → `kohustuse täitmine`), so the model uses real legalese instead of inventing it (bundled index: 5 core Riigi Teataja codes, obligations, civil procedure, property, penal, general; expandable) |
POS tag set: `S`=noun, `V`=verb, `A`=adj, `P`=pron, `D`=adv, `K`=adp,
`J`=conj, `N`=numeral, `I`=interj, `Y`=abbrev, `X`=foreign, `Z`=punct.
---
## ✨ Get started in 30 seconds (no install)
This section is for everyone, including if you've never opened a
terminal in your life. You'll be done before your tea is steeped.
**estonian-mcp is in Anthropic's official Connectors Directory**, so on
most Claude apps you can add it with **one click, no URL to paste, no
config, no auth.**
### One-click from the Connectors Directory (Cowork, claude.ai, Claude Desktop)
1. In your Claude app, open **Settings → Connectors**.
2. **Browse connectors** (the directory) and search **estonian**, it
shows up as the Estonian connector.
3. Click **Add** / **Connect**. That's it, the server is public, so
there's no authentication step.
4. Start a new chat and write in Estonian, proofread an email, study a
paragraph, draft a legal clause. Claude reaches for the tools whenever
it needs to verify spelling, lemmas, morphology, or legal phrasing
rather than guessing.
*(On older Claude Desktop without a Connectors menu, use the stdio path in
[Self-host (advanced)](#self-host-advanced).)*
### Prefer to paste the URL? (or don't see it in the directory yet)
Any MCP-over-HTTPS client can also connect directly to the hosted server:
we run it for you at `https://estonian-mcp.fly.dev/mcp`. In **Settings →
Connectors → Add custom connector**, paste:
```
https://estonian-mcp.fly.dev/mcp
```
Leave every "Authentication" / "API key" / "Bearer token" field **empty**
(the server is public, no token needed), then Save.
### In Claude Code (CLI)
One command, no clone, no Python, no `uv`. Point Claude Code at the
hosted server over HTTP:
```sh
claude mcp add --transport http estnltk https://estonian-mcp.fly.dev/mcp
```
Then run `/mcp` inside a session to confirm `estnltk` shows as
connected. The tools are live immediately, ask Claude to proofread
or lemmatize Estonian text and it'll reach for them.
Want a fully local, zero-network setup instead? See the stdio path
in [Self-host (advanced)](#self-host-advanced).
### In Codex (CLI)
One command, the same hosted server over streamable HTTP:
```sh
codex mcp add estonian --url https://estonian-mcp.fly.dev/mcp
```
`codex mcp list` should then show it as `enabled`. Auth reads as
"Unsupported", which is correct here: the server is public and wants no
token. Ask Codex to proofread or lemmatize Estonian and it calls the
tools directly.
### In ChatGPT
ChatGPT reaches MCP servers through a custom connector, which lives
behind developer mode. Turn that on in ChatGPT's settings, add a
connector pointing at `https://estonian-mcp.fly.dev/mcp` with no
authentication, then switch it on inside the chat that should use it. A
saved connector is not active until it is selected in the conversation.
OpenAI has moved that toggle between Settings sections more than once,
so follow their current
[developer mode article](https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt)
for the exact path rather than a menu name written down here.
One limit worth knowing: ChatGPT's *deep research* connectors expect a
server to expose `search` and `fetch` tools. This server exposes 26
Estonian NLP tools and neither of those, so it belongs in the ordinary
connector slot, not the deep research one.
### Don't see your client here?
Any tool that supports MCP over HTTPS can connect, just point it at
`https://estonian-mcp.fly.dev/mcp` with no auth. If your client only
speaks stdio (Cursor, VS Code MCP, Continue, Zed), jump to the
local-install path in [Self-host](#self-host-advanced).
---
## 💡 Pro tip, teach Claude *your* Estonian alongside the MCP
This MCP gives Claude **correct linguistics**: real lemmas, real case
forms, real spelling. What it can't do is teach Claude **your voice**:
the register, idioms, and tone you actually want when writing.
You handle the voice; the MCP handles the correctness. Layer them.
A few things to add to your Claude project / custom instructions /
system prompt to get this right:
- **Set the register.** *"Always reply in formal officialese Estonian
for legal and government topics, and in conversational Tallinn
speech for chat replies. Never mix the two in one message."*
- **Pin the dialect / region.** *"I'm from Tartu, prefer southern
Estonian phrasings where there's a choice (e.g. 'kus sa lähed'
rather than 'kuhu sa lähed' for casual speech)."*
- **Show your tone with examples.** Paste 3–4 short paragraphs of
your own writing into the project instructions and ask Claude to
match that voice. Real examples beat any abstract description.
- **Anchor common mistakes.** *"You always confuse `kasutama` (to use)
with `käsitlema` (to handle / to deal with). Double-check those
with the lemmatize tool before sending."*
- **Direct the MCP explicitly when it matters.** *"Before sending any
Estonian email, run spell_check on every word. Show me misspelled
words with suggestions before drafting."*
- **Use `classify_register` as a sanity check.** *"After drafting,
run classify_register on the final text and warn me if it lands
in 'formal' or 'colloquial' when I asked for the opposite."* The
classifier is coarse but reliably catches drift into officialese
(`käesolev`, `vastavalt`, `sätestama`) or slang (`mõnus`, `vinge`,
`kuule`).
- **Use `synonyms` to break repetition.** *"This newsletter uses
`kasutama` four times. Look up synonyms via the MCP and suggest
natural-sounding swaps."* You'll get real Estonian alternatives
with definitions, not invented ones.
- **Use `find_related_words` for richer rewrites.** *"What words
pattern with `kohv` in Estonian? Use that to suggest three
alternative phrasings for our café-launch ad copy."* This is
fastText-based, so it surfaces near-neighbours that aren't strict
synonyms, useful when you want adjacent concepts, not just
same-meaning swaps. (Quick rule of thumb: `synonyms` for "say the
same thing differently"; `find_related_words` for "what else
belongs in this conceptual space.")
The MCP catches misspelled words and invented case forms; your
prompt drives the style. Together they make Claude actually useful
for writing in Estonian, not just plausible-looking.
---
## How to prompt it once it's connected
Most prompts don't need to mention the tools by name, Claude picks
the right one. A few patterns that work especially well:
```
Proofread this Estonian email and use spell_check on any words
you're unsure about: <text>
```
```
Lemmatize this Estonian paragraph, then translate the lemmas to
English so I can study vocabulary: <text>
```
```
Analyze the morphology of this sentence and explain the case
markings: "Tallinnas elavad eestlased räägivad eesti keelt."
```
```
Extract the people and places from this Estonian news article,
then summarise in one paragraph.
```
```
This Estonian draft uses "kasutama" three times, look up synonyms
via the MCP and rewrite each occurrence with a natural-sounding
alternative that preserves the meaning.
```
```
Classify the register of this draft. If it scores formal, soften
it for a casual newsletter audience. If it scores colloquial,
tighten it for a B2B email.
```
The model calls the tool, gets authoritative output, and bases its
response on that, no more hallucinated lemmas or invented case forms.
---
## All clients at a glance
| Client | No-install path | Local-install path |
| --- | --- | --- |
| **Claude Cowork** | ✅ One click (directory) | ✅ stdio via JSON |
| **Claude Desktop** | ✅ One click (newer) | ✅ stdio via JSON |
| **claude.ai web** | ✅ One click (directory) | n/a |
| **Claude Code** (CLI) | ✅ `claude mcp add --transport http` | ✅ `claude mcp add ...` (stdio) |
| **Codex** (CLI) | ✅ `codex mcp add --url` | ✅ `codex mcp add -- <command>` (stdio) |
| **ChatGPT** | ✅ Custom connector (developer mode) | n/a |
| **Cursor** | ✅ Paste URL | ✅ stdio via JSON |
| **VS Code MCP / Continue / Zed** | n/a | ✅ stdio via JSON |
For Claude apps the no-install path is one click from the Connectors
Directory (search "estonian"). Codex takes the URL as a command, ChatGPT
takes it as a custom connector once developer mode is on, and other MCP
clients paste `https://estonian-mcp.fly.dev/mcp` in their Connectors UI. The
local-install path clones the repo and points the client at
`python server.py`.
## Reducing permission prompts
Claude clients ask for confirmation before calling a tool from a
custom/third-party connector, that's the client's security default,
not something the server controls (there's no MCP field a server can
send to suppress it). You'll especially see it right after adding or
updating the connector, since the client re-checks tools it hasn't
seen before.
Good news: **all 26 tools are marked `readOnlyHint: true`** (they only
read text, never write or call out), so any well-behaved client can
safely let you allow them once and stop asking:
- **Claude Desktop / Cowork / claude.ai**, when the prompt appears,
choose **"Always allow"** for the connector (or toggle it in the
connector's settings). One time, then it's quiet.
- **Claude Code**, run `/permissions` and allow the estonian-mcp
tools, or allow the whole server at once.
Re-releasing or updating the connector can reset that "always allow"
state (the client sees changed tools and re-asks), just allow it
again. A verified listing in the Anthropic Connectors Directory also
gets smoother permission UX than an unverified custom connector.
---
## Self-host (advanced)
The hosted instance is convenient, but if you'd rather run your own
(privacy, latency, custom auth, offline use), the same one-file
server works locally and as a container.
### Run locally as stdio (zero network)
EstNLTK requires Python 3.10–3.13.
```sh
git clone https://github.com/silly-geese/estonian-mcp.git
cd estonian-mcp
uv sync
uv run python scripts/fetch_resources.py # required, see below
uv run python tests/test_smoke.py # verify
```
**Don't skip the `fetch_resources.py` step.** `uv sync` installs Python
packages, but three of the things the server needs are *data*, not Python
distributions, so they can't live in `uv.lock`: NLTK's `punkt_tab`
tokenizer, Estonian WordNet (~26 MB), and the fastText embeddings
(~33 MB). Without them `check_compounds` and `check_term_consistency`
raise, `synonyms` refuses to run, and `check_term_consistency` reports
`degraded: true`. The script is idempotent, so re-running it is free.
The server **never downloads anything itself** — not at import, not on a
tool call. That's the [privacy promise](PRIVACY.md): no outbound HTTP from
the running process. Fetching is a separate step *you* run knowingly, and
the Docker image does the equivalent at build time.
Then wire it into your client.
**Claude Code:**
```sh
claude mcp add estnltk -- /absolute/path/to/uv \
--directory /absolute/path/to/estonian-mcp \
run python server.py
```
**Claude Desktop / Cowork (local mode)**, edit
`~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"estnltk": {
"command": "/absolute/path/to/uv",
"args": [
"--directory", "/absolute/path/to/estonian-mcp",
"run", "python", "server.py"
]
}
}
}
```
**Cursor**, same JSON shape in `~/.cursor/mcp.json`.
### Run as a remote server (HTTP)
The same `server.py` speaks `streamable-http` over the network.
Two auth postures:
- **Public mode** (`ESTNLTK_MCP_PUBLIC_MODE=1`), no bearer token,
per-IP rate limit (default 300/min). This is how the silly-geese
hosted instance runs.
- **Bearer mode** (default), every request must carry
`Authorization: Bearer <token>` (or Smithery's `?config=<base64>`);
per-token rate limit. Refuses to start without
`ESTNLTK_MCP_AUTH_TOKEN` ≥16 chars.
**Fly.io public deployment** (matches silly-geese):
```sh
fly auth login
fly apps create my-estonian-mcp
# one-time: persistent volume for /metrics counters (~$0.15/month)
fly volumes create estonian_mcp_data --size 1 --region ams -a my-estonian-mcp
fly deploy
```
`fly.toml` already sets `ESTNLTK_MCP_PUBLIC_MODE=1` and mounts the
volume at `/data`, so no token needed and `/metrics` counters survive
machine restarts. Endpoint: `https://my-estonian-mcp.fly.dev/mcp`.
**Fly.io with bearer auth**, remove
`ESTNLTK_MCP_PUBLIC_MODE` from `fly.toml`'s `[env]` block, then:
```sh
fly secrets set ESTNLTK_MCP_AUTH_TOKEN="$(python3 -c 'import secrets;print(secrets.token_urlsafe(32))')"
fly deploy
```
**Generic Docker** (any container host):
```sh
# Public
docker run -p 8081:8081 -e ESTNLTK_MCP_PUBLIC_MODE=1 \
ghcr.io/silly-geese/estonian-mcp # or build from source
# Bearer
docker run -p 8081:8081 \
-e ESTNLTK_MCP_AUTH_TOKEN="$(python3 -c 'import secrets;print(secrets.token_urlsafe(32))')" \
ghcr.io/silly-geese/estonian-mcp
```
**Behind nginx, with TLS and a token per client** — [`deploy/`](deploy/README.md)
is a Docker Compose stack (app + nginx + certbot) for running this on
your own host. nginx terminates TLS with Let's Encrypt certificates,
holds one bearer token per client with per-client rate limits and
one-line revocation, meters the requests it refuses as well as the ones
it serves, and answers OAuth discovery for connectors that cannot send a
static `Authorization` header. It writes no access log unless you turn
one on. See [`deploy/README.md`](deploy/README.md).
```sh
cp .env.example .env # set DOMAIN, LETSENCRYPT_EMAIL, INTERNAL_TOKEN
./deploy/new-token.sh my-laptop
./deploy/init-letsencrypt.sh
```
**Smithery** auto-builds from `smithery.yaml` and hosts the image
for you. Fork, [connect on Smithery](https://smithery.ai/docs/build),
deploy. The shipped `configSchema` is empty (one-click install)
because the deployment runs in public mode; flip it back if you fork
to a bearer-mode setup.
---
## Security
- **stdio mode**: pure local subprocess. No network egress, no shell
exec, no fs writes, no telemetry.
- **HTTP / public mode**: no auth required (intentional for the free
public service). Per-IP rate limit (300/min default). Same hardening
as bearer mode: no shell exec, no fs writes, no telemetry,
size-bounded inputs.
- **HTTP / bearer mode**: `ESTNLTK_MCP_AUTH_TOKEN` (≥16 chars)
required, server refuses to start without it. Bearer auth on every
request, constant-time comparison, per-token rate limit (120/min).
- **Common to all HTTP**: `/health` is the only unauthenticated path.
No request or token logging. `proxy_headers` is **off**: the server
reads `X-Forwarded-For` itself, counting
`ESTNLTK_MCP_TRUSTED_PROXY_HOPS` entries from the RIGHT (default 1,
for Fly's single edge proxy), because the leftmost entry is
caller-controlled and letting uvicorn trust it defeated the per-IP
rate limit (0.5.4).
- **Inputs**: 100 KB cap per text tool, 200 chars for `syllabify`.
Oversized inputs return a structured error rather than hanging.
- **Supply chain**: deps pinned + hashed in `uv.lock`. Dependabot
watches pip + GitHub Actions weekly. CI runs smoke + HTTP tests +
Docker build/boot on Python 3.11 and 3.13 on every push.
Full threat model and disclosure path: [SECURITY.md](SECURITY.md).
Privacy policy (what we receive, what we don't store): [PRIVACY.md](PRIVACY.md).
Terms of service for the hosted endpoint: [TERMS.md](TERMS.md).
---
## Notes
- **The server never downloads anything at runtime.** Not on import, not
on a tool call, not to fill a gap it notices. That's the
[privacy promise](PRIVACY.md). Resources are fetched at Docker build
time, or by you running `scripts/fetch_resources.py` on a source
install. If a resource is missing, tools say so — `synonyms` raises an
actionable error, and `check_term_consistency` returns
`degraded: true` with the reason in its Estonian summary rather than a
confident-looking partial answer.
- Most EstNLTK models (morph, NER, spell-check) ship inside the wheel.
Three things don't, because they're *data*, not Python distributions,
so `uv.lock` can't carry them: NLTK's `punkt_tab` tokenizer, WordNet,
and the fastText model.
- WordNet is a separate ~26 MB resource (used by `synonyms` and one of
`check_term_consistency`'s two rules).
- The fastText model used by `find_related_words` and
`check_compound_familiarity` is a ~33 MB compressed resource with a
100K-word vocabulary (built locally from Facebook's cc.et.300 via
compress-fasttext, CC-BY-SA-3.0; see [NOTICE](NOTICE)).
- Heavy neural taggers (`estnltk_neural`, BERT-based NER) are
intentionally not pulled in; this server stays lean and fast.
- First call after server start incurs a one-time tag-layer load
(~1–2 s). Subsequent calls are millisecond-scale.
- The hosted Fly instance scales to zero when idle; the first request
after a quiet period takes ~5 s, then everything is fast again.
## 🤝 Contributing
Contributions are welcome, especially from Estonian speakers who can
sharpen the linguistic rules. Here's how to get started:
1. **Fork** the repo and clone your fork.
2. **Set up** the environment (Python 3.10–3.13):
```sh
uv sync
# punkt_tab + WordNet + fastText — none can come from uv.lock:
uv run python scripts/fetch_resources.py
export ESTNLTK_MCP_FASTTEXT_PATH=~/.cache/estnltk-mcp/fasttext-et-medium
```
3. **Create a feature branch** (`git checkout -b feature/my-feature`).
4. **Run the tests**, both must pass:
```sh
uv run python tests/test_smoke.py # tool behaviour
uv run python tests/test_http.py # transport, auth, /metrics
uv run python tests/test_resources.py # resource-availability handling
```
5. **Commit** and open a pull request against `master`. CI (smoke on
Python 3.11 + 3.13, plus a Docker build/boot check) must be green
before merge.
**Please open an issue first for major changes** so we can discuss the
approach before you invest the work.
### Especially wanted: linguistic corrections
The heuristic tools lean on small hand-curated lexicons in
[`server.py`](server.py), marked/archaic words, register markers,
compound-split pairs, partitive-governing verbs, and the EKI
orthography rule sets. These are deliberately conservative and
incomplete. If you're a fluent Estonian speaker and spot a gap or a
wrong entry, that's the highest-value contribution you can make:
- A missing calque AI agents produce, with the idiomatic native form
- A verb that governs the partitive but isn't in the list
- A compound that should (or shouldn't) be flagged
- A register marker that's miscategorised
Open an issue with the English source (if it's a calque), the bad
Estonian, and the better Estonian, or send a PR adding the entry to
the relevant lexicon with a one-line test case.
## License
[Apache-2.0](LICENSE) for the source. Bundled data + models keep their
own (copyleft) licenses, these apply to those files only, not to the
Apache-2.0 code:
- **EstNLTK**, dual-licensed GPL-2.0 OR Apache-2.0 (we use Apache-2.0).
- **Vabamorf** analyzer, LGPL-2.1 with a separate commercial-use license.
- **Estonian fastText** model (`find_related_words`,
`check_compound_familiarity`), CC-BY-SA-3.0.
- **Estonian Wordnet** (`synonyms`), CC-BY-SA-4.0.
The CC-BY-SA model + Wordnet data carry share-alike obligations on
those files when you redistribute them (the Docker image includes
both). See [NOTICE](NOTICE) for full attribution and redistribution
terms.
TDQS
Scored across 26 tools
Every tool targets a distinct linguistic function, and even the large check_* family is cleanly separated by text type and specific rule (compounds, punctuation, hyphenation, capitalization, object case, legalese, officialese, style, register). The descriptions explicitly call out sibling tools and their boundaries, so an agent should not confuse them.
The check_* prefix is used consistently for quality heuristics, and most core NLP tools follow a readable verb_noun or single-verb pattern. Minor deviations like spell_check, named_entities, and common_legal_usage break the strict pattern, but the naming is still predictable and mostly uniform.
26 tools is above the typical 3-15 MCP-server band, but the breadth of the Estonian language domain—morphology, orthography, style, legal text, and register—justifies most of them. A few tools could be consolidated, but none feel like padding for the stated purpose.
The tool surface covers analysis, generation, spelling, orthography, style, register, and legal language comprehensively, with no obvious dead ends. It is not a perfect 5 because several tools are explicitly phase-1 heuristics (e.g. partial compound, punctuation, and object-case coverage), so some corrections require workarounds rather than authoritative full-language checking.