- Python 97.9%
- Dockerfile 2.1%
| klett_mcp | ||
| .env.example | ||
| .gitignore | ||
| Dockerfile | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
klett-mcp
An unofficial MCP server for reading digital textbooks published on bg.izzi.digital — Klett/BulVest 2000's platform for Bulgarian schools. Sibling project to eprosveta-mcp (a different publisher, different platform, different quirks) — built for the same reason: a parent's homework-helper assistant that can look up which textbook covers a school subject, find the right lesson by name, and read the actual page.
Important: no login is used here, and that's worth understanding
Unlike eprosveta-mcp, this client sends no credentials at all. Every
endpoint it calls was verified reachable with a bare, cookie-less HTTP GET —
including a page 130 books deep into a 134-page book. That's very likely
not an intentional free tier: bg.izzi.digital/#/... (the app a logged-in
user actually browses) gates finding a book behind a Klett ID login, but
the exported book content itself lives on a separate static path
(/DOS/{dosId}/datastore/blocks/{blockId}/...) meant to be iframed from
inside that logged-in app — and nothing on that path actually checks the
session. If you have a direct link (because you or someone with a paid
account opened it and copied the URL), the login is simply never enforced.
Because of that, this client never enumerates, guesses, or brute-forces
ids. catalog.json is a small, hand-maintained list — add an entry only
for a book whose direct link you (or the account holder) actually opened
yourself. Don't point this at ids you found by scanning ranges.
Tools
| Tool | What it does |
|---|---|
list_books(subject=None) |
The curated catalog — no live search API exists on this platform. Optional subject substring filter. |
get_book_info(book_id) |
Page count and metadata for a book. |
get_table_of_contents(book_id) |
Lesson title → page range, from the book's own bookmarks. |
get_page_url(book_id, page) |
A direct, no-login, clickable link to a specific page — safe to hand to a student directly. |
get_page_image(book_id, page) |
A page rendered to a ready-to-read PNG, returned inline to the model. |
get_page_image_url(book_id, page) |
A plain image URL for the same page — embed it as a markdown image to show a human, not just the model. |
There's no get_page_text here (unlike eprosveta-mcp) — this platform's
pages are vector art with per-glyph symbol references, not real Unicode
text, and its own full-text search index (search.json) came back empty for
every page checked on both books tested. get_page_image/get_page_image_url
are the only ways to actually read a page: both fetch the page's SVG, inline
every image it references as a data: URI, and rasterize with
rsvg-convert server-side — verified pixel-perfect (tables, illustrations,
correct text, no OCR needed).
get_page_image alone won't show a human anything — an MCP tool's image
result is vision input for the model, and most chat frontends never
re-surface a tool result to the person on the other end (confirmed against
Open WebUI's Claude pipe: it only ever attaches files the agent writes to
disk). If you want a human to actually see the page, use
get_page_image_url instead.
Running it
cp .env.example .env # fill in a random bearer token
docker build -t klett-mcp .
docker run --env-file .env -p 8801:8801 klett-mcp
Or locally without Docker (needs rsvg-convert on PATH — librsvg2-bin on
Debian/Ubuntu, librsvg via Homebrew or most Linux package managers):
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export MCP_AUTH_TOKEN=...
python3 -m klett_mcp.server
The server speaks MCP over Streamable HTTP at http://<host>:8801/mcp.
Every request must carry Authorization: Bearer <MCP_AUTH_TOKEN> — there's
no per-user auth, just one shared secret between the server and whatever
trusted client(s) you point at it. Put it behind your own network/firewall;
don't expose it to the open internet as-is.
get_page_image_url needs two more settings: PUBLIC_BASE_URL (the
externally-reachable URL this server is proxied at) and
PUBLIC_IMAGE_TOKEN — a separate secret from MCP_AUTH_TOKEN, gating
just the /public/page-image/... route by query string (a plain
<img src> can't send a header). Kept separate on principle and for
symmetry with eprosveta-mcp, even though this platform's own content needs
no login at all.
Configuration
See .env.example for the full list. Notably KLETT_CATALOG_PATH: point it
at your own JSON file (same shape as the bundled klett_mcp/catalog.json) to
add books without rebuilding the image — each entry needs id, title,
subject, grade, dos_id, and block_id (the last two come from the
book's landing-page URL and its embedded clientData.pdf.data.fields.index
pointer — see client.py's module docstring for exactly where to find them).
What this is not
This reads content from a URL you already have access to via a real account, the same way opening that link in a browser would. It doesn't crack the login or bypass an access check that's actually being enforced — the check for these specific export paths just isn't there. It's a convenience layer for a model to navigate content you already have legitimate access to, not a way to redistribute copyrighted textbook content, and it's deliberately scoped to never discover ids on its own.
License
MIT — see LICENSE.