
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
edupage-mcp-full
Advanced tools
Full-feature EduPage MCP server: timetables, grades, homework, meals (ordering), messages, role-aware student switching, 2FA, multi-school with automatic student discovery, tiered name matching, and student cache.
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.
Two other EduPage MCP servers already exist:
mrtineu/edupage-mcp — also
published on PyPI as edupage-mcpmhlavac/edupage-mcpBoth are good and I have no affiliation with them — they are simply referenced here for honest comparison. They primarily focus on the read-only surface of the API.
This project deliberately goes further:
| Capability | mhlavac | mrtineu (PyPI) | this project |
|---|---|---|---|
Advanced login — portal auto-detect, 2FA, session id (PHPSESSID) | partial (portal) | basic only | ✅ |
| Timetables (own + any teacher/class/room) | ✅ | ✅ | ✅ |
| Grades (all / by term & year) | ✅ | ✅ | ✅ |
| Substitutions / timetable changes | ✅ | ✅ | ✅ |
| Meals — read menu | ✅ | ✅ | ✅ |
| Meals — choose / sign-off / rate | ❌ | ❌ | ✅ |
Send messages (send_message) | ✅ | ❌ | ✅ |
| Parent student switching (switch to/from student) | partial (list) | ❌ | ✅ |
| Next ringing time / bell schedule | ❌ | ❌ | ✅ |
| Raw session custom request | ❌ | ❌ | ✅ |
| Multiple schools (auto-login + discovery) | ❌ | ❌ | ✅ |
| Role-aware (parent / student / teacher) | ❌ | ❌ | ✅ |
| Day summaries (one-call daily report) | ❌ | ❌ | ✅ |
Key differentiators:
EDUPAGE_SUBDOMAINS with one shared
login and the server auto-discovers students across all schools — no need to
maintain a manual "Student → school1, Student → school2" mapping. A student at
two schools (e.g. Student at school1 + school2) is found automatically
with separate per-school results.get_student_timetable
switches to the student account for parents, returns direct timetables for
students. No tool duplication.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_schools (logged-in
schools with role/user id per school + env config). 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_schools (logged-in schools with role 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 edupage-mcp-full
# or, if you prefer pip (into whatever environment your MCP client uses):
pip install edupage-mcp-full
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.
uvx --from "git+https://github.com/oliverhruby/edupage-mcp.git" edupage-mcp-full
# or
pip install "git+https://github.com/oliverhruby/edupage-mcp.git"
Option D — Docker
Use this for an isolated container runtime.
Requirements: Docker.
Pull a prebuilt image (recommended):
docker pull ghcr.io/oliverhruby/edupage-mcp:latest
docker run --rm -i \
-e EDUPAGE_USERNAME=your_username \
-e EDUPAGE_PASSWORD=your_password \
ghcr.io/oliverhruby/edupage-mcp:latest
Version tags are also available (for example v0.4.0) if you prefer pinned
images.
Build locally from source (fallback):
docker build -t edupage-mcp-full .
docker run --rm -i \
-e EDUPAGE_USERNAME=your_username \
-e EDUPAGE_PASSWORD=your_password \
edupage-mcp-full
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+.
git clone https://github.com/oliverhruby/edupage-mcp.git
cd edupage-mcp
uv sync # or: python -m venv .venv && .venv/bin/python -m pip install -e .
uv run edupage-mcp-full
Option D — Docker
Use this for an isolated container runtime.
Requirements: Docker.
Pull a prebuilt image (recommended):
docker pull ghcr.io/oliverhruby/edupage-mcp:latest
docker run --rm -i \
-e EDUPAGE_USERNAME=your_username \
-e EDUPAGE_PASSWORD=your_password \
ghcr.io/oliverhruby/edupage-mcp:latest
Version tags are also available (for example v0.4.0) if you prefer pinned
images.
Build locally from source (fallback):
docker build -t edupage-mcp-full .
docker run --rm -i \
-e EDUPAGE_USERNAME=your_username \
-e EDUPAGE_PASSWORD=your_password \
edupage-mcp-full
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).
# Windows (persistent, per-user)
setx EDUPAGE_USERNAME "your_username"
setx EDUPAGE_PASSWORD "your_password"
setx EDUPAGE_SUBDOMAINS "s1,s2,s3" # optional: multiple schools (auto-login + discovery)
# macOS / Linux
export EDUPAGE_USERNAME="your_username"
export EDUPAGE_PASSWORD="your_password"
export EDUPAGE_SUBDOMAINS="s1,s2,s3" # optional
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):
{
"mcp": {
"edupage": {
"type": "local",
"enabled": true,
"command": ["uvx", "edupage-mcp-full"],
"env": {
"EDUPAGE_USERNAME": "{env:EDUPAGE_USERNAME}",
"EDUPAGE_PASSWORD": "{env:EDUPAGE_PASSWORD}",
"EDUPAGE_SUBDOMAINS": "{env:EDUPAGE_SUBDOMAINS}"
}
}
}
}
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_schools | Connected status, 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:
setx EDUPAGE_SUBDOMAINS "school1,school2,school3" # Windows
export EDUPAGE_SUBDOMAINS="school1,school2,school3" # macOS / Linux
get_schools # lists school1, school2, school3 (logged in, with role)
scan_students # discovers Student A and Student B across those schools
get_student_timetable name="Student A" # is found at school1 AND school2
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):
login_all subdomains="school1,school2" usernames="u1,u2" passwords="p1,p2"
get_my_timetable subdomain="school1"
get_my_timetable subdomain="school2"
get_schools # shows all logged-in subdomains + which is active
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:
"timetable for Student A" -> get_student_timetable name="Student A"
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_schools | Logged-in schools + 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_schools | List logged-in schools + role 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:
mkdir -p ~/.config/opencode/skills/school-day-summary
cp <site-packages>/edupage_mcp/skills/school-day-summary/SKILL.md \
~/.config/opencode/skills/school-day-summary/SKILL.md
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).
FAQs
Full-feature EduPage MCP server: timetables, grades, homework, meals (ordering), messages, role-aware student switching, 2FA, multi-school with automatic student discovery, tiered name matching, and student cache.
The pypi package edupage-mcp-full receives a total of 531 weekly downloads. As such, edupage-mcp-full popularity was classified as not popular.
We found that edupage-mcp-full demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.