No description
  • Python 97.9%
  • Dockerfile 2.1%
Find a file
2026-09-30 18:38:02 +00:00
klett_mcp Shrink get_page_image's tool result to avoid CLIJSONDecodeError 2026-09-30 19:09:33 +03:00
.env.example Add get_page_image_url: a plain, human-visible page image URL 2026-09-26 17:17:49 +03:00
.gitignore feat: initial klett-mcp -- MCP server for bg.izzi.digital textbooks 2026-09-26 14:57:19 +03:00
Dockerfile feat: initial klett-mcp -- MCP server for bg.izzi.digital textbooks 2026-09-26 14:57:19 +03:00
LICENSE feat: initial klett-mcp -- MCP server for bg.izzi.digital textbooks 2026-09-26 14:57:19 +03:00
pyproject.toml Shrink get_page_image's tool result to avoid CLIJSONDecodeError 2026-09-30 19:09:33 +03:00
README.md Add get_page_image_url: a plain, human-visible page image URL 2026-09-26 17:17:49 +03:00
requirements.txt Shrink get_page_image's tool result to avoid CLIJSONDecodeError 2026-09-30 19:09:33 +03:00

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.