No description
  • Python 97.6%
  • Dockerfile 2.4%
Find a file
2026-09-30 18:37:38 +00:00
eprosveta_mcp Shrink get_page_image's tool result to avoid CLIJSONDecodeError 2026-09-30 19:08:06 +03:00
.env.example Add get_page_image_url: a plain, embeddable image URL for humans 2026-09-26 17:12:17 +03:00
.gitignore Initial MCP server for bg.e-prosveta.bg 2026-09-26 09:45:36 +03:00
Dockerfile Initial MCP server for bg.e-prosveta.bg 2026-09-26 09:45:36 +03:00
LICENSE Initial MCP server for bg.e-prosveta.bg 2026-09-26 09:45:36 +03:00
pyproject.toml Shrink get_page_image's tool result to avoid CLIJSONDecodeError 2026-09-30 19:08:06 +03:00
README.md Add get_page_image_url: a plain, embeddable image URL for humans 2026-09-26 17:12:17 +03:00
requirements.txt Shrink get_page_image's tool result to avoid CLIJSONDecodeError 2026-09-30 19:08:06 +03:00

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_image returns 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_text is 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 — use get_page_image for that.
  • get_page_image alone 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, use get_page_image_url instead — 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.