The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the EduPage MCP Server listing page.
A Model Context Protocol (MCP) server that exposes the full functionality of the
edupage-api Python library to AI
agents such as opencode, Claude, Cursor and any other MCP client.
EduPage is a school information system used across Europe. This server lets you query and operate a student / teacher / parent EduPage account directly from your agent: timetables, grades, homework, substitutions, meals (including ordering), messages, rosters, parent child-switching and more — including multiple schools (e.g. two children attending different schools).
⚠️ Unofficial API. Like all EduPage MCP servers, this relies on the community-maintained
edupage-apilibrary, which talks to EduPage's undocumented endpoints. Use read-only features freely; use the write features (send_message, meal ordering, child switching) carefully.
Three EduPage MCP servers exist. All three wrap the same community-maintained
edupage-api library — none of them
reimplements EduPage's undocumented endpoints from scratch. I have no
affiliation with the other two; they are listed here because an honest
comparison is more useful than a marketing page.
| this project | mrtineu/edupage-mcp | mhlavac/edupage-mcp | |
|---|---|---|---|
| Install | uvx edupage-mcp-full, pip, or one-click from the MCP Registry | uvx edupage-mcp (PyPI) | clone + uv sync |
| Tools | 29 (consolidated — see note) | 13 | 26 |
| License | MIT | Apache-2.0 | GPL-3.0 |
| Stars | 1 | 2 | 1 |
| Repo created | 2026-09-03 | 2026-06-05 | 2026-02-21 |
| Last push | 2026-09-28 | 2026-09-16 | 2026-09-22 |
| Test suite | ✅ (pytest, run in CI) | ❌ | ✅ |
Two things worth stating plainly before the feature table. This is the
newest and least-adopted of the three — as of 2026-09-28 it is 25 days old with
a single star, while mhlavac/edupage-mcp predates it by over six months. And
tool count is a poor metric: this project deliberately folds families behind
a discriminating parameter (get_roster(roster_type=…),
get_timeline(category=…)) rather than shipping near-identical tools, so its 29
tools cover ground that takes the others 26 or 13 separate ones. The table
below is therefore written by capability, not by tool name.
| Capability | mhlavac | mrtineu | this project |
|---|---|---|---|
| Login — credentials | ✅ | ✅ | ✅ |
| Login — portal auto-detect | ✅ | ❌ | ✅ |
| Login — 2FA completion | ❌ | ❌ | ✅ |
Login — existing PHPSESSID | ❌ | ❌ | ✅ |
| Multiple schools in one deployment | ✅ | ❌ | ✅ |
| Timetable — own | ✅ | ✅ | ✅ |
| Timetable — by student | ✅ (by name) | ❌ | ✅ (name or id) |
| Timetable — by class | ✅ | ❌ | ✅ |
| Timetable — by teacher | ❌ | ❌ | ✅ |
| Timetable — by classroom | ❌ | ❌ | ✅ |
| Timetable — over a date range | ❌ | ❌ | ✅ |
| Next-week timetable | ✅ | ❌ | ✅ |
| Grades | ✅ | ✅ | ✅ |
| Substitutions / timetable changes | ✅ | ✅ | ✅ |
| Missing teachers | ❌ | ✅ (experimental) | ✅ |
| Meals — read menu | ✅ | ✅ | ✅ |
| Meals — choose / sign off / rate | ❌ | ❌ | ✅ |
| Send messages | ✅ (open bug #5) | ❌ | ✅ |
| Parent — list own children | ✅ | ❌ | ✅ |
| Parent — switch session into a child | ❌ | ❌ | ✅ |
| Bell schedule (periods) | ✅ | ❌ | ✅ |
| Next ringing time (live) | ❌ | ❌ | ✅ |
| Homework — from notifications | ✅ | ✅ | ✅ |
| Homework — body text + attachments | ❌ | ✅ | ❌ |
| Download a homework file to disk | ❌ | ✅ | ❌ |
| Absences | ✅ | ❌ | ✅ |
| Upcoming events | ✅ | ❌ | ✅ |
| School news | ✅ | ❌ | ✅ |
| Assignments (tests / exams) | ✅ | ❌ | ✅ |
| Roster — students/teachers/classes/classrooms/subjects | ✅ | partial | ✅ |
| Whole-school roster | ✅ | ❌ | ✅ |
| "Is the kid at school today?" | ✅ | ❌ | ❌ |
| Aggregate summary | ✅ (last N days) | ❌ | ✅ (one day) |
| Raw custom HTTP request | ❌ | ❌ | ✅ |
| Role detection (parent/student/teacher) | ❌ | ❌ | ✅ |
| Auto re-login on expired session | ❌ | ✅ | ✅ (several tools) |
| Automated tests in CI | lint only | ❌ | ✅ |
The table above is not one-sided, and it would be dishonest to leave it there:
get_school_days — answers "is my kid at school today / over lunch?" from
trips, excursions and absences. This project has no equivalent tool.school field, and accept a school= filter. Here,
subdomain selects a single school, and a few tools (for example
get_student_timetable) return one result per school instead of merging..mcp.json for zero-config Claude Code pickup.download_homework_file saves the file to disk. This
project's get_timeline(category='homework') — like mhlavac's — only sees what
the timeline notification itself exposes. This is a real gap here.uvx install without a clone
(this project matches the install story, and is additionally in the MCP
Registry).PHPSESSID session.choose_meal, sign_off_meal, rate_meal.get_next_ringing_time — the next bell, live.custom_request — a raw passthrough for any endpoint no tool covers yet.get_day_summary — one call for a whole day: timetable, substitutions,
meals, homework, absences, news and events.mhlavac/edupage-mcp.mrtineu/edupage-mcp.Comparison checked against each project's source, GitHub metadata and PyPI on 2026-09-28. These projects move fast — if a row is wrong, please open an issue rather than assuming it is deliberate.
A single stdio MCP server exposing 29 tools (published on PyPI as
edupage-mcp-full):
login (by credentials, portal auto-detect, or a
PHPSESSID cookie via method=), login_all (multi-school, one call),
two_factor_finish (complete a pending 2FA), get_subdomains (schools
available to the account with role/user id per school + env config, live
subdomain discovery for parents). The former auth_status,
user_id, login_auto, login_from_session, and
two_factor_check_confirmed tools are folded into these.get_my_timetable, get_timetable (teacher/student/class/
classroom; end_date for a range, formerly get_timetable_range),
get_student_timetable (student by name, cross-school), get_next_week_timetable,
get_next_ringing_time, get_periods, get_school_yearfind_student (name → person_id, cross-school),
get_student_timetable (cross-school, role-aware), scan_students
(auto-discover all students across schools), get_my_students (classmates or
school-wide for parents), switch_to_student (by id or name, parent only),
switch_to_parent, clear_student_cache (force refresh cached student lists)get_subdomains (subdomains available to the account — live-discovered for parents, limited by EDUPAGE_SUBDOMAINS when set — plus session state per school)get_gradesget_timeline (category= for homework,
assignments, absences, events, news, or full history since a date)get_timetable_changes, get_missing_teachersget_meals, choose_meal, sign_off_meal, rate_mealget_day_summary (one call: timetable, substitutions,
missing teachers, grades, meals, homework, assignments, absences, news,
events, notifications for a date — "what happened yesterday at school" in a
single round trip; each section is isolated so one failure doesn't kill the
report). Includes an OpenCode skill (school-day-summary) for human-readable
formatting in OpenCode; other clients use the raw JSON directly.get_roster (roster_type= for students, all students,
teachers, classes, classrooms, or subjects), get_my_studentssend_message, switch_to_student, switch_to_parent, custom_requestYou need an MCP-capable client (opencode, Claude Desktop, Cursor, etc.).
If you are using an AI coding client, a simple prompt is often enough to get started, for example: "Install the EduPage MCP as described in this GitHub repository oliverhruby/edupage-mcp". Most MCP-capable clients can then guide you through the available setup options.
Option A — from MCP Registry (recommended, one-click in VS Code / GitHub Copilot)
The server is listed in the MCP Registry.
In VS Code or GitHub Copilot, search for "EduPage MCP" and install with one click.
Or use the direct deeplink: mcp://install/io.github.oliverhruby/edupage-mcp
Option B — from PyPI
Use this for normal usage with a released version.
Requirements: uv for uvx, or Python 3.10+ for pip.
uvx runs the package without a persistent install. If uvx is unavailable,
install uv first (pip install uv or winget install astral-sh.uv).
Option C — from GitHub (latest source)
Use this if you want the latest changes before a PyPI release.
Requirements: uv for uvx, or Python 3.10+ for pip.
Option D — Docker
Use this for an isolated container runtime.
Requirements: Docker.
Pull a prebuilt image (recommended):
Version tags are also available (for example v0.4.0) if you prefer pinned
images.
Build locally from source (fallback):
The container uses the same environment variables described in
Configure credentials. It also includes a
HEALTHCHECK (stdio process liveness by default; local TCP check in HTTP
transport modes).
For HTTP transports, set optional runtime vars:
MCP_TRANSPORT: stdio (default), sse, or streamable-httpMCP_HOST: bind host (default 127.0.0.1)MCP_PORT: bind port (default 8000)MCP_API_KEY: optional bearer token for HTTP authWhen MCP_API_KEY is set, HTTP requests must include Authorization: Bearer <key>.
If MCP_API_KEY is not set, HTTP endpoints are unauthenticated. For production,
prefer proper authentication and TLS via a reverse proxy or API gateway.
pyproject.tomlpinsmcp<2(the stable FastMCP v1 API).mcp 2.xrenamedFastMCPtoMCPServerand changed the API surface; this server targets the FastMCP v1 API for simplicity and stability.
Option C — development from source
Use this if you are contributing or debugging locally.
Requirements: Python 3.10+.
Option D — Docker
Use this for an isolated container runtime.
Requirements: Docker.
Pull a prebuilt image (recommended):
Version tags are also available (for example v0.4.0) if you prefer pinned
images.
Build locally from source (fallback):
The container uses the same environment variables described in
Configure credentials. It also includes a
HEALTHCHECK (stdio process liveness by default; local TCP check in HTTP
transport modes).
For HTTP transports, set optional runtime vars:
MCP_TRANSPORT: stdio (default), sse, or streamable-httpMCP_HOST: bind host (default 127.0.0.1)MCP_PORT: bind port (default 8000)MCP_API_KEY: optional bearer token for HTTP authWhen MCP_API_KEY is set, HTTP requests must include Authorization: Bearer <key>.
If MCP_API_KEY is not set, HTTP endpoints are unauthenticated. For production,
prefer proper authentication and TLS via a reverse proxy or API gateway.
pyproject.tomlpinsmcp<2(the stable FastMCP v1 API).mcp 2.xrenamedFastMCPtoMCPServerand changed the API surface; this server targets the FastMCP v1 API for simplicity and stability.
Either set environment variables or pass credentials to login (see
Prompt examples).
Single school? Just set EDUPAGE_USERNAME + EDUPAGE_PASSWORD. The server
auto-discovers your school via the EduPage portal on startup — no subdomain needed.
Multiple schools? Add EDUPAGE_SUBDOMAINS (comma-separated). The server
logs into all of them on startup with your shared credentials.
opencode — add to ~/.config/opencode/opencode.json (or opencode.jsonc):
Put credentials in your shell/environment (or a
.env) and reference them with{env:VAR}, or hardcode them underenv:directly.uvxwill auto-provision the package the first time; it must be on yourPATH.
Claude Desktop / Cursor — use claude_desktop_config.json /
.mcp.json with a mcpServers entry in the standard shape, pointing
command/args at the venv python and the edupage_mcp.py path, plus an
env block with your credentials.
After editing client config, restart the client so the MCP server is loaded.
| User prompt | Likely tool call(s) | Expected response |
|---|---|---|
| "Are we connected and logged in?" | get_subdomains | Available school subdomains (live-discovered for parents), active school/subdomain, env config, and login state per school. |
| "What classes do I have today?" | get_my_timetable | A short timetable summary for today. |
| "Show me the 9.A schedule for 2026-09-10" | get_timetable target_type="class" target_id="9.A" date_str="2026-09-10" | Class timetable for that date. |
| "What grades do I have this term?" | get_grades term="FIRST" year=2026 | Subject-by-subject grade overview for the selected term/year. |
| "Any substitutions today?" | get_timetable_changes | Changes, cancellations, and replacements for today. |
| "What is for lunch and order option 2 for tomorrow" | get_meals → choose_meal date_str="2026-09-10" meal_type="lunch" number=2 | Meal menu and order confirmation (or a clear error if unavailable). |
| "Find Student A's timetable for tomorrow" | get_student_timetable name="Student A" date_str="2026-09-10" | Student A's timetable; if found in multiple schools, one result per school. |
| "List teachers and send a hello to Teacher456" | get_roster roster_type="teachers" → send_message recipient_id="Teacher456" body="Hello!" | Teacher list plus message sent confirmation. |
| "What happened at school yesterday for my kids?" | get_day_summary date_str="2026-09-09" (discovery index) → get_day_summary date_str="2026-09-09" name="Student A" subdomain="school-a" → ... name="Student B" subdomain="school-b" | Discovery-first: the no-name call lists each child per school; then one complete daily report call per child (timetable, substitutions, missing teachers, grades, meals, homework, assignments, absences, news, events, notifications). Keeps each response small and avoids mixing schools/students. |
| "How was school today for Student A?" | get_day_summary name="Student A" (defaults to today) | Human-readable summary via the bundled OpenCode skill school-day-summary. |
Each subdomain (school) keeps its own logged-in session. There are two ways to log in to several schools at once:
A) Automatic on startup (recommended). Set EDUPAGE_SUBDOMAINS (a
comma-separated list) plus the shared EDUPAGE_USERNAME / EDUPAGE_PASSWORD —
the server logs into all of them when it launches, so every tool is immediately
ready and students are discoverable across all schools with no login call and
no student→school mapping:
B) On demand with login_all. Authenticate several schools at once, then pass
subdomain to any data tool (it defaults to the last active subdomain when
omitted):
You can also call login once per school to add/lookup sessions incrementally.
Single school? No
EDUPAGE_SUBDOMAINSneeded — the server auto-discovers your school via the portal on startup. For two or more schools, setEDUPAGE_SUBDOMAINS(auto-login) or uselogin_all/ repeatedlogincalls.
Because the server auto-discovers students across the configured
EDUPAGE_SUBDOMAINS (or every logged-in school when the variable is unset),
you don't need to know or state which school a student is in. Just ask for the
timetable by name and the server searches every school in scope:
get_student_timetable (with no subdomain):
EDUPAGE_SUBDOMAINS, or all logged-in schools when unset — for a student
whose first/last/full name matches (scan_students does just the discovery
step),A student attending more than one school (e.g. Student at school1 +
school2) therefore yields a list of two per-school timetables — separate
results, never merged. This is the built-in replacement for maintaining a
manual "Student → school1" mapping: with EDUPAGE_SUBDOMAINS set, discovery is
fully automatic.
| Tool | Description | Writes? |
|---|---|---|
login | Log in with username/password; method="credentials" (default), method="auto" (portal auto-detect, formerly login_auto), or method="session" with a PHPSESSID cookie (formerly login_from_session). Env vars supported. | ✅ session |
login_all | Log in to multiple schools in one call | ✅ session |
two_factor_finish | Finish a pending 2FA login (email/app code or poll_seconds device confirmation; formerly two_factor_check_confirmed + two_factor_finish) | ✅ session |
get_subdomains | Subdomains available to the account (live-discovered for parents; limited to EDUPAGE_SUBDOMAINS when set) + role/user id per school, active subdomain, failed logins, env config (formerly auth_status, user_id) | |
get_school_year | Current school year | |
get_my_timetable | Logged-in user's timetable for a date | |
get_timetable | Timetable of a teacher/student/class/classroom; end_date for a daily range (formerly get_timetable_range) | |
get_student_timetable | Student's timetable by name or id (role-aware, cross-school) | ✅ session |
get_next_week_timetable | Mon–Fri timetable for next week | |
get_next_ringing_time | Next bell (break/lesson) at a given time | |
get_periods | Bell schedule (period start/end times) | |
get_grades | Grades, optionally by year & term | |
get_timeline | Timeline notifications, one category= at a time: recent, history (since date_from), homework, assignments, absences, events, news (formerly get_notifications, get_notification_history, get_homework, get_assignments, get_absences, get_upcoming_events, get_news) | |
get_timetable_changes | Substitutions / timetable changes for a date | |
get_missing_teachers | Teachers missing on a date | |
get_day_summary | One-call daily report (timetable, substitutions, teachers, grades, meals incl. breakfast/dinner when published, homework, assignments, absences, news, events, notifications) for a date; student by name/id (role-aware). Discovery-first: parent without name/student_id returns a lightweight per-school student index (mode:"discovery"); pass full=True to build full reports for every child. Bundles OpenCode skill school-day-summary for human-readable output. | |
get_meals | Meal menu (all 5 slots: breakfast, snack, lunch, afternoon snack, dinner) | |
choose_meal | Order a meal | ✅ |
sign_off_meal | Cancel an ordered meal | ✅ |
rate_meal | Rate a meal (quality/quantity) | ✅ |
get_roster | One roster_type= at a time: students (logged-in user's class), all_students (whole school, short list), teachers, classes, classrooms, subjects (formerly get_students, get_all_students, get_teachers, get_classes, get_classrooms, get_subjects) | |
get_my_students | Students visible to the logged-in account (one school) | |
find_student | Look up a student's person_id by name (cross-school) | |
scan_students | Auto-discover students across the configured EDUPAGE_SUBDOMAINS (or all logged-in schools when unset) | |
clear_student_cache | Clear cached student rosters (one school or all schools) | ✅ cache |
get_subdomains | Available school subdomains + role/session per school | |
send_message | Send a message to a user | ✅ |
switch_to_student | Switch to a student account by id or name (parent only) | ✅ session |
switch_to_parent | Switch back to the parent account | ✅ session |
custom_request | Raw request through the active session (GET/POST) | ✅ |
get_timeline categories homework, assignments, absences, events and
news derive their data from the timeline notifications — if the school
doesn't push certain event types, those categories may return empty lists.get_missing_teachers is marked experimental upstream (parses HTML from
the substitution page) and can raise if a teacher's name no longer matches.rate_meal and ordering depend on the school publishing menus with the
matching identifiers; not all schools expose ratings.get_meals first tries the per-student meal-ordering endpoint (needed for
ordering/ratings). When a school doesn't enable that, it falls back to the
school's public canteen menu widget (/menu/?wid=menu_CanteenMenu_1).
All five slots (breakfast/snack/lunch/afternoon_snack/dinner) are always
returned; slots the school doesn't publish are None.The package includes an OpenCode skill (school-day-summary) at
<site-packages>/edupage_mcp/skills/school-day-summary/SKILL.md. It teaches
OpenCode agents how to turn get_day_summary JSON into a human-readable daily
school report.
OpenCode only: To register it:
Restart OpenCode; the agent can then answer "what happened at school yesterday
for my kids?" by calling get_day_summary per child.
Other MCP clients (Copilot, Claude, Cursor, etc.) — call get_day_summary
directly; they receive the full structured JSON. Formatting is client-specific
(no skill system in the MCP protocol).
Contributor and maintainer guidance is in CONTRIBUTING.md.
edupage-api, not this wrapper.login method="session" with the resulting PHPSESSID.EDUPAGE_SUBDOMAINS, login_all, or repeated login calls).
If a school is not logged in, that student's results from that school cannot
be discovered.If you like this project and want to support or request a feature, send me a beer, it keeps my mind relaxed and ideas will come :-)
MIT © Oliver Hrubý
This project is not affiliated with or endorsed by Ascora (EduPage) or by
the authors of edupage-api. EduPage is a registered trademark of its
respective owner(s).