- Python 97.6%
- Dockerfile 2.4%
| eprosveta_mcp | ||
| .env.example | ||
| .gitignore | ||
| Dockerfile | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
eprosveta-mcp
An unofficial MCP server for browsing and reading your own digital textbook/notebook library on bg.e-prosveta.bg — a Bulgarian publisher's platform (Просвета) with no public API.
Built for a parent building a homework-helper assistant for their kid: point an MCP-capable model at this server and it can look up which notebook covers a school subject/grade, find the right lesson by name, and read the actual page — no manual browsing required.
Why this exists
There's no public API, so every endpoint here was found by downloading the site's Angular bundle and reading its source. Two things stood out enough to be worth documenting for anyone using this:
get_page_imagereturns a server-side flattened page — background, text and graphics already composited into one PNG by e-prosveta's own backend, pixel-perfect, correct spacing, real embedded fonts. No OCR, no client-side rendering. This is the tool to reach for when a model actually needs to read a page.get_page_textis lossy. The site's export pipeline (pdf2htmlEX-style) discarded the space character between words — a real word gap and intra-word letter-kerning use the identical near-zero-width filler markup, so stripping tags glues words together (Билковаградинаимаправоъгълна...instead ofБилковата градина има правоъгълна...). The words themselves are correct and complete, just unspaced. Useful for keyword search across many pages; not for reading prose — useget_page_imagefor that.get_page_imagealone won't show a human anything. An MCP tool's image result is vision input for the model — most chat frontends never re-surface it to the person on the other end (confirmed against Open WebUI's Claude pipe: it only ever attaches files the agent writes to disk, and does nothing with a tool result beyond feeding it back to the model). If you want a human to actually see the page, useget_page_image_urlinstead — a plain HTTP image URL, gated by its own separate token, meant to be embedded directly as a markdown image in a reply your frontend renders normally.
Tools
| Tool | What it does |
|---|---|
search_books(grade, book_type, subject=None) |
Find books/notebooks by grade + type (textbook, notebook, teacher_book, assistance_book, other_book, dictation); 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. The reliable way to map a topic to a page. |
get_page_text(book_id, page) |
Raw page text — keyword search only, see caveat above. |
get_page_image(book_id, page) |
A page as 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. |
Running it
cp .env.example .env # fill in your e-prosveta.bg login + a random bearer token
docker build -t eprosveta-mcp .
docker run --env-file .env -p 8800:8800 -v eprosveta-data:/data eprosveta-mcp
Or locally without Docker:
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export EPROSVETA_EMAIL=... EPROSVETA_PASSWORD=... MCP_AUTH_TOKEN=...
python3 -m eprosveta_mcp.server
The server speaks MCP over Streamable HTTP at http://<host>:8800/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 (see above) needs two more settings if you want it to
work: PUBLIC_BASE_URL (the externally-reachable URL this server is
proxied at — e.g. behind your own reverse proxy on a subdomain) and
PUBLIC_IMAGE_TOKEN, a separate secret from MCP_AUTH_TOKEN gating
just the /public/page-image/... route (query-string, not header — a plain
<img src> can't send one). Deliberately separate: MCP_AUTH_TOKEN can
call every tool this server has; PUBLIC_IMAGE_TOKEN is expected to end up
in a visible chat message, so it must not double as the real key.
Configuration
All configuration is environment variables — see .env.example for the full
list and defaults. Notably EPROSVETA_DEVICE_HASH_FILE: this client persists
a device fingerprint to disk and reuses it across restarts (mount /data as
a volume in Docker). Don't regenerate it per run — e-prosveta.bg appears
to gate/reject login attempts from a brand-new device_hash shortly after an
already-established one on the same account ({"error": "device_not_authorized"}), so a stable value matters. A fresh value is fine
the first time it's ever used for an account.
What this is not
This talks to e-prosveta.bg using your own account's own login, exactly as the website itself does — it doesn't bypass any paywall or DRM. It's a convenience layer for a model to navigate content you already have access to in your account, not a way to redistribute copyrighted textbook content.
License
MIT — see LICENSE.