by silly-geese
Offers offline Estonian natural‑language processing tools—including spelling correction, morphological analysis, lemmatization, POS tagging, named‑entity extraction, synonym lookup, fastText‑based related‑word search, register classification, and a suite of style and legal checks—exposed via the Model Context Protocol for direct consumption by LLM clients.
Estonian MCP provides a locally hosted server that wraps the EstNLTK toolkit together with EKI orthography rules and public‑domain legal texts. It supplies 26 read‑only NLP utilities tailored for Estonian, allowing large language models to obtain authoritative linguistic information without any external API calls.
https://estonian-mcp.fly.dev/mcp (no authentication required).uv sync and uv run python scripts/fetch_resources.py, then start the server with python server.py for stdio mode or use the Docker image for HTTP mode. Configuration supports public mode or bearer‑token authentication.spell_check(text), analyze_morphology(text)) within prompts; the LLM will automatically call the appropriate endpoint.Q: Do I need an API key? A: No. The public instance runs in public mode and accepts unauthenticated requests. Self‑hosted deployments can enable bearer‑token authentication if desired.
Q: Which Python versions are supported? A: Python 3.10 through 3.13.
Q: How large are the required data files? A: Approximately 26 MB for Estonian WordNet and 33 MB for the fastText model, plus a small NLTK tokenizer file.
Q: Can I use the server without Docker?
A: Yes. Run it locally via stdio (python server.py) after installing dependencies with uv.
Q: What is the latency like? A: After the initial model load (1–2 s), individual tool calls complete in milliseconds; the first request after idle may take ~5 s on the Fly.io host.
Q: Is the server safe for production? A: The server enforces input size limits (100 KB for text tools), rate limits per IP or token, and contains no outbound network calls, making it suitable for secure deployments.
Claude is quite bad at Estonian, so this MCP is here to fix that. Give it a shot.
A small Model Context Protocol server that exposes 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_etgold dataset (a noun-phrase inflection benchmark; Lillepalu & Alumäe, arXiv:2510.21193), 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 indata/inflection_et_eki_disputes.json.uv run python scripts/apply_eki_corrections.pyrebuilds 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_etcovers four cases, so ten were never measured. Against Pert Lomp's käänamiskorpus (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 (raamatutelebefore the literaryraamatuile). 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:
| 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) |
Attestation check, for each compound noun, reports whether the lemma is in the corpus vocabulary or Estonian WordNet and returns its top fastText neighbours. Unattested is ordinary for specialist vocabulary and is not a rewrite signal; the narrow is_suspect flag needs an unattested lemma whose nearest real neighbour is still under 0.55 (mõtteliin-style translationese, literal English "train of thought" → real Estonian is mõttekäik) |
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.
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.
(On older Claude Desktop without a Connectors menu, use the stdio path in Self-host (advanced).)
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.
One command, no clone, no Python, no uv. Point Claude Code at the
hosted server over HTTP:
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).
One command, the same hosted server over streamable HTTP:
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.
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 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.
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.
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:
kasutama (to use)
with käsitlema (to handle / to deal with). Double-check those
with the lemmatize tool before sending."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).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.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.
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.
| 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.
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:
/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.
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.
EstNLTK requires Python 3.10–3.13.
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: 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:
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:
{
"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.
The same server.py speaks streamable-http over the network.
Two auth postures:
ESTNLTK_MCP_PUBLIC_MODE=1), no bearer token,
per-IP rate limit (default 300/min). This is how the silly-geese
hosted instance runs.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):
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:
fly secrets set ESTNLTK_MCP_AUTH_TOKEN="$(python3 -c 'import secrets;print(secrets.token_urlsafe(32))')"
fly deploy
Generic Docker (any container host):
# 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/
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.
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,
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.
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)./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).syllabify.
Oversized inputs return a structured error rather than hanging.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. Privacy policy (what we receive, what we don't store): PRIVACY.md. Terms of service for the hosted endpoint: TERMS.md.
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.uv.lock can't carry them: NLTK's punkt_tab tokenizer, WordNet,
and the fastText model.synonyms and one of
check_term_consistency's two rules).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).estnltk_neural, BERT-based NER) are
intentionally not pulled in; this server stays lean and fast.Contributions are welcome, especially from Estonian speakers who can sharpen the linguistic rules. Here's how to get started:
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
git checkout -b feature/my-feature).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
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.
The heuristic tools lean on small hand-curated lexicons in
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:
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.
Apache-2.0 for the source. Bundled data + models keep their own (copyleft) licenses, these apply to those files only, not to the Apache-2.0 code:
find_related_words,
check_compound_familiarity), CC-BY-SA-3.0.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 for full attribution and redistribution terms.
Please log in to share your review and rating for this MCP.
Explore related MCPs that share similar capabilities and solve comparable challenges
by modelcontextprotocol
A Model Context Protocol server for Git repository interaction and automation.
by zed-industries
A high‑performance, multiplayer code editor designed for speed and collaboration.
by modelcontextprotocol
Model Context Protocol Servers
by modelcontextprotocol
A Model Context Protocol server that provides time and timezone conversion capabilities.
by cline
An autonomous coding assistant that can create and edit files, execute terminal commands, and interact with a browser directly from your IDE, operating step‑by‑step with explicit user permission.
by upstash
Provides up-to-date, version‑specific library documentation and code examples directly inside LLM prompts, eliminating outdated information and hallucinated APIs.
by daytonaio
Provides a secure, elastic infrastructure that creates isolated sandboxes for running AI‑generated code with sub‑90 ms startup, unlimited persistence, and OCI/Docker compatibility.
by continuedev
Enables faster shipping of code by integrating continuous AI agents across IDEs, terminals, and CI pipelines, offering chat, edit, autocomplete, and customizable agent workflows.
by github
Connects AI tools directly to GitHub, enabling natural‑language interactions for repository browsing, issue and pull‑request management, CI/CD monitoring, code‑security analysis, and team collaboration.