Skip to main content
Glama
EduPage MCP Server

EduPage MCP Server

EduPage MCP Server

GitHub release Glama Grade Quality gates Security Container security Coverage drift

Project

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-api library, which talks to EduPage's undocumented endpoints. Use read-only features freely; use the write features (send_message, meal ordering, child switching) carefully.


Related MCP server: ecole-directe-mcp

Table of Contents


Why another EduPage MCP server?

Two other EduPage MCP servers already exist:

Both 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)

❌

❌

✅

Homework — body text + attachments

❌

✅

✅

Download a homework file to disk

❌

✅

✅

Key differentiators:

  • Multi-school automatic discovery. Set 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.

  • Role-aware tools. The server detects whether you're a parent, student, or teacher at each school and behaves accordingly — get_student_timetable switches to the student account for parents, returns direct timetables for students. No tool duplication.

  • Full write surface. Meal ordering/rating, message sending, student switching — the other servers don't cover these.

Comparison checked 2026-09-28. If a row is wrong, please open an issue.


What it provides

A single stdio MCP server exposing 31 tools (published on PyPI as edupage-mcp-full):

  • Authentication — 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.

  • Timetables — 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_year

  • Students — find_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)

  • Schools — get_subdomains (subdomains available to the account — live-discovered for parents, limited by EDUPAGE_SUBDOMAINS when set — plus session state per school)

  • Grades — get_grades

  • Timeline / notifications — get_timeline (category= for homework, assignments, absences, events, news, or full history since a date)

  • Homework — get_timeline(category='homework') to list assignments (ids in additional_data.superid), get_homework_material to read one assignment's body text, dates and attachment list (the notification alone only carries the title and due date), download_attachment to save one attachment to disk

  • Substitutions — get_timetable_changes, get_missing_teachers

  • Meals — get_meals, choose_meal, sign_off_meal, rate_meal

  • Day summaries — get_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). A bundled agent skill (school-day-summary) turns that JSON into a human-readable daily report — install it with edupage-mcp-full install-skill (see Skills); every client can also call get_day_summary directly and read the JSON.

  • Rosters — get_roster (roster_type= for students, all students, teachers, classes, classrooms, or subjects), get_my_students

  • Actions — send_message, switch_to_student, switch_to_parent, custom_request


Getting started

You need an MCP-capable client (opencode, Claude Desktop, Cursor, etc.).

1. Install

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 — as an agent plugin (recommended: the server and the skill in one install)

For VS Code, GitHub Copilot and Claude Code this repo ships a plugin that installs the server together with the school-day-summary agent skill:

copilot plugin marketplace add oliverhruby/edupage-mcp
copilot plugin install edupage@edupage-plugins

Claude Code reads the same marketplace:

claude plugin marketplace add oliverhruby/edupage-mcp
claude plugin install edupage@edupage-plugins

opencode, Cursor and Windsurf read Agent Skills but not plugins — for those, and for every other install route, see docs/PLUGINS.md.

Option B — one-shot with APM (the server and the skill in one command, optional, emerging standard)

APM (Agent Package Manager) is an emerging open standard for distributing agent context — think npm for agent skills, MCP servers and instructions. This route is optional: Option A and every route below work exactly as before. If you already use APM (or want to try it), it is the only cross-harness one-shot install here.

  1. Install the CLI once. Requirements: git, plus uv for the server runtime.

    • Windows: winget install --id Microsoft.APM -e (or scoop bucket add apm https://github.com/microsoft/scoop-apm; scoop install apm)

    • macOS / Linux: brew install apm

    • Any platform with Python 3.10+: pip install apm-cli

    • Other options: installation docs

  2. Install this package:

    apm install oliverhruby/edupage-mcp

    One command does both:

    • deploys the school-day-summary skill into the harness's skills directory (.claude/skills/, .agents/skills/, ...);

    • writes the uvx edupage-mcp-full stdio server entry into the MCP config of every harness APM detects (Claude Code, GitHub Copilot, Cursor, opencode, Codex, Gemini, Windsurf, Kiro, ...); add --target claude (or another target) to force one harness.

  3. Verify:

    apm mcp list        # server registered?

    apm uninstall oliverhruby/edupage-mcp removes the package and the deployed skill files, but leaves the MCP server entry it wrote into .mcp.json (and other harness configs) — delete that entry yourself if you want it gone.

Credentials are never embedded in the package: set EDUPAGE_USERNAME / EDUPAGE_PASSWORD (and EDUPAGE_SUBDOMAINS for multi-school accounts) before running apm install — see 2. Configure credentials.

Pinning. Append #<sha> (or #<tag> once a release contains apm.yml) to pin an exact revision and silence APM's "dependency unpinned" warning; without a pin, APM records the resolved commit in apm.lock.yaml. Releases up to v0.7.0 predate the manifest, so use a commit sha until the next release.

The Claude Code plugin/marketplace install (Option A) remains supported and unchanged — APM is an additional install route, declared in this repo's apm.yml.

Option C — from MCP Registry (one-click, server only, no skill)

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 D — 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 E — 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 F — 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-http

  • MCP_HOST: bind host (default 127.0.0.1)

  • MCP_PORT: bind port (default 8000)

  • MCP_API_KEY: optional bearer token for HTTP auth

When 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.toml pins mcp<2 (the stable FastMCP v1 API). mcp 2.x renamed FastMCP to MCPServer and changed the API surface; this server targets the FastMCP v1 API for simplicity and stability.

Option G — 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

2. Configure credentials

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.

3. Register with your MCP client

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 under env: directly. uvx will auto-provision the package the first time; it must be on your PATH.

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.


Prompt examples

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 once you install the bundled school-day-summary skill (edupage-mcp-full install-skill).


Multiple schools & automatic student discovery

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_subdomains   # 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_subdomains         # 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_SUBDOMAINS needed — the server auto-discovers your school via the portal on startup. For two or more schools, set EDUPAGE_SUBDOMAINS (auto-login) or use login_all / repeated login calls.


Students by name (e.g. "timetable for Student A")

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):

  1. searches every school in the discovery scope — the configured 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),

  2. for each school where the student is found, switches to the student account if you're logged in as a parent, returns that student's timetable for the date, and switches back to the parent account afterwards,

  3. returns one result per school.

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 reference

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_homework_material

Full body text, dates and attachment list of one assignment, by superid from get_timeline(category='homework') → additional_data.superid

download_attachment

Download any authenticated attachment to disk — absolute URL, or the relative /elearning/… key from get_timeline; refuses school/login pages (default <tempdir>/homework, never overwrites)

✅ disk

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 the school-day-summary agent skill (installed with edupage-mcp-full install-skill) 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 text request through the active session (GET/POST); refuses binary bodies and points at download_attachment

✅


Data & safety notes

  • Most tools are read-only. The ones marked Writes? ✅ mutate EduPage state (sent messages, ordered meals, switched accounts). Use them with care.

  • download_attachment is the one tool that writes to local disk: it saves into dest_dir (default <tempdir>/homework), takes the name from the server's content-disposition when present, and appends (1), (2), ... rather than overwriting anything. An HTML login/error page is refused instead of being written out as an attachment.

  • custom_request returns text only: a binary body is refused with a pointer to download_attachment, and a text body over 60000 characters comes back truncated with an explicit truncated: true.

  • 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.

  • Meal 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.


Skills

The package ships one agent skill (school-day-summary). It teaches a coding agent how to turn get_day_summary JSON into a human-readable daily school report — "what happened yesterday for my kids?"

Installing the plugin ships this skill together with the server. To install the skill on its own:

# already installed with the server:
edupage-mcp-full install-skill

# ...or with nothing but uv:
uvx --from edupage-mcp-full edupage-mcp-full install-skill --agent copilot

Each agent it touches prints one line:

installed -> C:\Users\you\.copilot\skills\school-day-summary\SKILL.md
skipped (identical) -> /home/you/.claude/skills/school-day-summary/SKILL.md
skipped (exists, differs, use --force) -> /home/you/.agents/skills/school-day-summary/SKILL.md

Restart your agent afterwards so it picks the skill up.

Options

Option

Effect

--agent {opencode,copilot,claude,agents,all}

Install into that agent only. With no flag: every agent directory that already exists. all forces all four and creates the missing directories.

--print

Print the resolved SKILL.md path and exit — writes nothing.

--dry-run

Report what would be written, write nothing.

--force

Overwrite an existing SKILL.md that differs.

Where it goes

--agent

Installed to

opencode

~/.config/opencode/skills/

copilot

~/.copilot/skills/

claude

~/.claude/skills/

agents

~/.agents/skills/ — the cross-vendor location (Copilot, Codex, Cursor and opencode all honour it)

Re-running is safe: a file that is already identical is reported as skipped (identical), and one that differs is left alone unless you pass --force, so a skill you edited locally is never silently clobbered.

Prefer to place the file yourself? edupage-mcp-full install-skill --print prints the exact path of the bundled SKILL.md in the installed package.

Other MCP clients (Claude, Cursor, or any client without a skill system) can just call get_day_summary directly — they receive the full structured JSON. Formatting is client-specific. The base MCP protocol has no skill concept (an io.modelcontextprotocol/skills extension is specified, but this server does not implement it yet), which is why distribution goes through the plugin and the installer above. See docs/PLUGINS.md for every install route.


Contributing

Contributor and maintainer guidance is in CONTRIBUTING.md.

  • Contribution workflow and local setup

  • Architecture and implementation details

  • Release process (PyPI, GitHub Releases, GHCR)

  • CI quality gates and upstream coverage drift checks


Limitations

  • Unofficial/read-mostly by design. EduPage can change its endpoints at any time; reliability ultimately depends on edupage-api, not this wrapper.

  • No CAPTCHA bypass. If EduPage presents a CAPTCHA during login, log in via browser first, then use login method="session" with the resulting PHPSESSID.

  • 2FA requires human interaction (approve on device or provide a code).

  • Parent/teacher accounts are only partially verified upstream; some parent methods are best-effort.

  • The auth session lives for the lifetime of the MCP server process; restarting the client means logging in again.

  • Cross-school student discovery depends on being logged into all relevant schools (via 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.


Support

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 :-)

Support via PayPal


License

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).

Available Tools

31 tools
choose_mealA
Idempotent

Order/choose a meal. Writes: books the selected menu for the date.

Args: date_str: YYYY-MM-DD to order for. meal_type: 'snack' | 'lunch' | 'afternoon_snack'. number: 1-based menu choice among the chooseable menus (see get_meals). subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'ordered': True, meal_type, date, number}.

Notes: - Read get_meals first for the date to pick a valid number. - To cancel, use sign_off_meal.

Inputs: number (required), date_str (required), meal_type (required), subdomain.

clear_student_cacheA
Idempotent

Force refresh of cached student data. Writes: drops the local cache so the next student lookup re-fetches from EduPage. Call this after students are added/removed from a school, or if scan_students/find_student seems stale.

Args: subdomain: School whose cache to clear. Without it, clears ALL schools.

Returns: dict: {'cleared': <subdomain|'all'>, 'entries_removed': }.

Inputs: subdomain.

custom_requestA
Destructive

Send a raw request to the Edupage server using the active session. Can perform writes depending on the endpoint — treat as write-capable.

Args: url: Absolute URL, or a path like '/export/ajax_prevedene_meno.php' (resolved against https://<subdomain>.edupage.org). method: 'GET' or 'POST'. data: Request body (for POST). headers: JSON string of extra headers, e.g. '{"Accept": "application/json"}'. subdomain: School whose session to use (defaults to the active).

Returns: dict: {'status_code': int, 'content_type': str, 'bytes': int, 'text': str, 'truncated': bool}. bytes is the raw body length and text is it decoded.

Notes: - Low-level escape hatch for endpoints not covered by the dedicated tools — prefer those when available. Parse the returned text yourself; fields are not pre-serialized. - Text only. A binary body (an Office/PDF/image/video attachment) is refused rather than returned as lossily-decoded errors="replace" garbage; use download_attachment(url=…, subdomain=…), which writes the raw bytes to disk and accepts any authenticated attachment URL. - text is capped at 60000 characters so the cap is this tool's, not the MCP client's silent one. EduPage's own pages ignore Range, so a truncated text body cannot be paged through: narrow the request instead (a more specific path or a dedicated tool), or save the whole body with download_attachment.

Inputs: url (required), method (required), data, headers, +1 more.

download_attachmentA
DestructiveIdempotent

Download one attachment from the school to disk. Writes: creates a local file.

Saves any authenticated attachment — homework material, message and event attachments alike, which is what get_timeline puts in additional_data.attachements and get_homework_material lists — as the raw response bytes, never overwriting (a ' (1)', ' (2)', ... suffix is appended instead). School pages are not attachments: an HTML or login page is refused rather than written out. Because the bytes go to disk untouched, this is the only tool that returns a binary attachment intact; custom_request refuses one.

Args: url: Absolute attachment URL, or a school-relative path such as '/elearning/ruqjzfpv?z%3A…' — the exact form get_timeline(category='recent') returns in additional_data.attachements — resolved against the school origin. Any authenticated attachment URL works, not just homework: message and event attachments included. The request is authenticated with the school's session. dest_dir: Directory to save into, created when missing. Defaults to <tempdir>/homework (e.g. .../AppData/Local/Temp/homework). filename: Save under this name instead of the server-suggested one. Directory components and characters illegal on the filesystem are stripped, so the file always lands directly inside dest_dir. subdomain: School whose session authenticates the request (defaults to the active subdomain).

Returns: dict: {'saved_to', 'bytes', 'source_url', 'name'}.

Notes: - The only tool in this server that writes to disk, and the only one excluded from the read-only e2e suite — call it only on request. - Verified live 2026-10-02: a bad or expired attachment token is a plain HTTP 404, and a school page fetched without a session answers HTTP 200 with the login page. Both raise instead of writing a file, so a saved attachment is always real bytes. - The name comes from content-disposition when the server sends it, otherwise from the URL path. - Bounded by the library session's 5 s request timeout (Edupage(request_timeout=5)), so very large attachments can fail.

Inputs: url (required), dest_dir, filename, subdomain.

find_studentA
Read-onlyIdempotent

Look up a student by first/last/full name using tiered matching. Read-only. Without a subdomain, searches ALL logged-in schools and returns one result per school where the student is found.

Args: name: First, last or full student name (also 'Novák V.' short names). subdomain: Restrict the search to one school (default: all logged-in schools in scope).

Returns: dict with results: [{name, student_id, class_id, subdomain, tier, confidence}] sorted by confidence. Tiers: 1=exact, 2=first name, 3=last name, 4=substring.

Notes: - Use a student_id from the results with get_student_timetable / get_day_summary for unambiguous lookups. - Ambiguous matches surface all candidates instead of guessing.

Inputs: name (required), subdomain.

get_day_summaryA
Read-onlyIdempotent

One-call daily school report for a date (default today): timetable, substitutions, missing teachers, grades received that day, meals, homework, assignments, absences, news, events, and timeline notifications.

Composes the individual section tools so you don't need to fire 8-10 calls to answer "what happened yesterday at school" or "what's coming tomorrow".

  • If name/student_id is provided: report for that specific student (found across all schools unless subdomain scopes it).

  • If omitted: discovery-first — for a parent this returns a lightweight per-school index of the account's children (no per-child section fetching), so you can then call per child with name/student_id. Set full=True to instead build the full report for every child.

  • If omitted and logged in as student/teacher: report on the logged-in account. Every section is isolated — a failure in one section yields {"ok": false, "error": ...} without failing the report.

Args: date_str: YYYY-MM-DD to report on (default today). name: Student to report for, by first/last/full name. Ambiguous names surface every candidate instead of guessing. Ignored when student_id is given; see find_student to resolve an id first. student_id: person_id of the student (preferred, unambiguous). Found across all schools unless subdomain scopes the lookup. subdomain: School to report on (defaults to the active subdomain). full: When no name/student_id is given and the account is a parent, build the full report for every child instead of returning the lightweight per-school index. Costs one section sweep per child.

Inputs: full, name, date_str, subdomain, +1 more.

get_gradesA
Read-onlyIdempotent

Get grades for the logged-in student. Read-only.

Args: year: School-year start year to filter by (e.g. 2025 for 2025/26). term: 'FIRST' or 'SECOND' to restrict the term. subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'subdomain', 'grades': [serialized grades with subject, teacher, percent, ...]}.

Notes: - When both year and term are omitted returns the current gradebook. - Use get_school_year to resolve the current school-year start.

Inputs: term, year, subdomain.

get_homework_materialA
Read-onlyIdempotent

Full body text and attachment list of one homework/assignment material. Read-only.

Fetches the school's material-player page for superid and flattens its widget tree into plain text plus absolute attachment URLs — the timeline notification carries only a short summary, never the assignment body or the attachment list, so this is the only way to read what the assignment says.

Args: superid: Material id, taken from additional_data.superid of a notification returned by get_timeline(category='homework'). subdomain: School whose session to use (defaults to the active subdomain).

Returns: dict: {'subdomain', 'superid', 'title', 'details', 'date_from', 'date_to', 'content', 'attachments'}. Each attachment is {'name', 'url'} with an absolute URL — hand url to download_attachment to save it.

Notes: - Discover ids first: get_timeline(category='homework') (or category='assignments') and read additional_data.superid. For a whole day of homework plus everything else prefer get_day_summary. - An invalid, expired or invisible id returns an actionable error, not a stack trace. - content is plain text; an enabled student upload area is rendered as '[Student answer / file upload area]'.

Inputs: superid (required), subdomain.

get_mealsA
Read-onlyIdempotent

Get the meal menu for a date. Read-only. Always returns all five meal slots (breakfast, snack, lunch, afternoon_snack, dinner) — slots not published by the school are None.

Args: date_str: YYYY-MM-DD (default today). subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'date', 'subdomain', 'meals': {breakfast/snack/lunch/ afternoon_snack/dinner: menus}}. Each menu carries chooseable/ordered info usable with choose_meal / sign_off_meal.

Notes: - Tries the personal ordering endpoint first; when the school hasn't enabled it, falls back to the school's public canteen menu widget.

Inputs: date_str, subdomain.

get_missing_teachersA
Read-onlyIdempotent

Get teachers missing on a date (default today). Read-only.

Args: date_str: YYYY-MM-DD (default today). subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'date', 'subdomain', 'teachers': [serialized missing teachers]}. Empty list when no teacher is missing.

Notes: - Pair with get_timetable_changes for the full substitution picture.

Inputs: date_str, subdomain.

get_my_studentsA
Read-onlyIdempotent

Get the students visible to the logged-in account. Read-only: parent accounts see their linked children (parsed from the school homepage); student/teacher accounts see classmates. Uses cached data.

Args: subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'subdomain', 'students': [{person_id, name, class_id, ...}]} usable with switch_to_student and get_student_timetable.

Notes: - Prefer get_my_students over get_roster to see your children / classmates; get_roster(roster_type='all_students') lists the whole school. - Cache is refreshed by clear_student_cache; scan_students returns the same visibility across all schools.

Inputs: subdomain.

get_my_timetableA
Read-onlyIdempotent

Get the timetable for the logged-in user on a date. Read-only.

Args: date_str: YYYY-MM-DD (default today). subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'date', 'subdomain', 'lessons': [serialized lessons]}.

Notes: - For another student use get_student_timetable (by name/id); for a teacher/class/room on a date or a range use get_timetable. - Next week for yourself: get_next_week_timetable.

Inputs: date_str, subdomain.

get_next_ringing_timeA
Read-onlyIdempotent

Get the type (break/lesson) and time of the next school-bell ringing. Read-only.

Args: date_time_str: ISO datetime to search onward from (default: now). subdomain: School to query (defaults to the active subdomain).

Returns: Serialized ringing: type (break/lesson) and time.

Notes: - See get_periods for the full bell schedule.

Inputs: subdomain, date_time_str.

get_next_week_timetableA
Read-onlyIdempotent

Get the Mon-Fri timetable for next week for the logged-in user, grouped by weekday. Read-only.

Args: subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'monday', 'subdomain', 'week': [{weekday, date, lessons} x5]}.

Notes: - Weekdays are 'Po','Ut','St','Št','Pi'. - For the logged-in user on a single day use get_my_timetable; for any target (teacher/class/room/student) over a range use get_timetable with end_date.

Inputs: subdomain.

get_periodsA
Read-onlyIdempotent

Get the bell schedule (periods with start/end times). Read-only.

Args: subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'periods': [{'starttime', 'endtime'}, ...]}.

Notes: - Combine with get_next_ringing_time for live bell timing.

Inputs: subdomain.

get_rosterA
Read-onlyIdempotent

Get a school roster: students, teachers, classes, classrooms or subjects. Read-only.

Args: roster_type: Which roster to return: - 'students' — students in the logged-in user's class (formerly get_students). - 'all_students' — a short list of all students in the school (formerly get_all_students). - 'teachers' — all teachers (formerly get_teachers). - 'classes' — all classes (formerly get_classes). - 'classrooms' — all classrooms (formerly get_classrooms). - 'subjects' — all subjects (formerly get_subjects). subdomain: School to query (defaults to the active subdomain).

Returns: dict keyed by the roster name, e.g. {'subdomain': ..., 'teachers': [...], ...}.

Notes: - For the students visible to the logged-in account (parents: their linked children; students: classmates) prefer get_my_students. - To look one student up by name use find_student. - Returned person/class ids feed get_timetable (target_type/target_id) and switch_to_student.

Inputs: roster_type (required), subdomain.

get_school_yearA
Read-onlyIdempotent

Return the current school year (starting year). Read-only.

Args: subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'school_year': , 'subdomain': ...}.

Inputs: subdomain.

get_student_timetableA
Read-onlyIdempotent

Get a student's timetable by first/last name OR person_id. Read-only. Without a subdomain, searches every school in the discovery scope (the configured EDUPAGE_SUBDOMAINS, or all logged-in schools when unset) and returns one result per school where the student is found — so a student attending multiple schools yields separate per-school timetables.

Args: name: Student's first/last/full name. student_id: person_id (preferred — unambiguous, see find_student). date_str: YYYY-MM-DD (default today). subdomain: Restrict to one school (default: all logged-in schools).

Returns: dict: {'results': [{student, student_id, class_id, date, subdomain, lessons}]}; with a query/matched_schools summary when more than one school is searched.

Notes: - If logged in as a parent this resolves the child agent-side and queries their timetable directly (no session switching). For your own timetable use get_my_timetable. - Use the student_id from find_student / get_my_students for unambiguous lookups.

Inputs: name, date_str, subdomain, student_id.

get_subdomainsA
Read-onlyIdempotent

List the school subdomains available to the logged-in account and the server's session status per school, plus overall login state. Read-only.

Args: None. This tool takes no arguments — the active credentials and EDUPAGE_SUBDOMAINS determine the result entirely.

Returns: dict with: - subdomains: list of school subdomains available to the account. For a logged-in parent account this is live-discovered (via edupage-api's get_subdomains, read from the profile page) and includes schools with no session yet; it is limited to EDUPAGE_SUBDOMAINS when that is set (allowlist), otherwise every accessible school is listed. For student/teacher accounts or when not logged in it falls back to the configured / current sessions. - schools: per subdomain {subdomain, logged_in, role (student/parent/teacher), user_id, two_factor_pending, active}. When EDUPAGE_SUBDOMAINS is set it contains only the in-scope schools (sessions for unconfigured schools are never listed). - active_subdomain: the school used by tools without an explicit subdomain argument. - failed_logins: subdomain → error for login attempts that failed or are blocked (e.g. pending 2FA at startup). - env_*_set: whether EDUPAGE_USERNAME / EDUPAGE_PASSWORD / EDUPAGE_SUBDOMAINS are configured.

Notes: - Use this instead of the former auth_status / user_id tools. - To connect to a discovered subdomain that has no session yet, pass it to login_all. - A school with two_factor_pending: True needs two_factor_finish.

Inputs: no named parameters.

get_timelineA
Read-onlyIdempotent

Get EduPage timeline notifications, filtered by category. Read-only.

Args: category: Which event types to return: - 'recent' (default) — all currently visible notifications (homework, tests, messages, grades, events...). - 'history' — all notifications since date_from (incl. older ones). - 'homework' — homework assignments (formerly get_homework). - 'assignments' — homework, tests, exams and projects (formerly get_assignments). - 'absences' — absence records (formerly get_absences). - 'events' — upcoming events: trips, excursions, meetings, holidays... (formerly get_upcoming_events). - 'news' — school news (formerly get_news). date_from: YYYY-MM-DD. Only meaningful for category='history'. subdomain: School to query (defaults to the active subdomain).

Returns: dict with subdomain and the matching notifications, e.g. {'subdomain': ..., 'notifications': [...]} (key is the category name).

Notes: - Categories are derived from timeline notifications; a school that doesn't publish a given event type returns an empty list. - For a whole-day report (timetable, substitutions, meals, homework, events, news, grades) prefer get_day_summary — one call.

Inputs: category, date_from, subdomain.

get_timetableA
Read-onlyIdempotent

Get the timetable for a teacher, student, class or classroom on a date (or a date range, see end_date). Read-only.

Args: target_type: 'teacher' | 'student' | 'class' | 'classroom'. target_id: person/class/classroom id as returned by get_roster. date_str: Single day, YYYY-MM-DD (default today). Ignored when end_date is given. end_date: When set, returns the timetable for every day from date_str (default today) to end_date inclusive, keyed by date — the equivalent of the former get_timetable_range. subdomain: School to query (defaults to the active subdomain).

Returns: Single-day shape: {'target', 'date', 'subdomain', 'lessons'}. Range shape: {'subdomain', 'range': {: single-day result}}. Days with no published data get an empty lessons list.

Notes: - For the logged-in user's own timetable prefer get_my_timetable; for a student by name/id use get_student_timetable. - Any school's whole-week plan for yourself: get_next_week_timetable.

Inputs: target_id (required), target_type (required), date_str, end_date, +1 more.

get_timetable_changesA
Read-onlyIdempotent

Get substitution/timetable changes for a date (default today). Read-only.

Args: date_str: YYYY-MM-DD (default today). subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'date', 'subdomain', 'changes': [serialized substitutions]}. Empty list when nothing changed or the school publishes none.

Notes: - Pair with get_missing_teachers for the full substitution picture. - For one student's plan on a day use get_student_timetable / get_timetable.

Inputs: date_str, subdomain.

loginA
Idempotent

Log in to Edupage for a school. Writes: establishes (or replaces) the server-side session for that subdomain.

Args: username: EduPage account login. Falls back to EDUPAGE_USERNAME. password: Account password. Falls back to EDUPAGE_PASSWORD. subdomain: School subdomain (e.g. 'school'). Falls back to the first value of EDUPAGE_SUBDOMAINS; for method='auto' it is optional and tags the detected school's session. method: How to authenticate: - 'credentials' (default) — username/password for a known subdomain. - 'auto' — portal auto-detect of the school (formerly login_auto). - 'session' — build a session from an existing PHPSESSID cookie (formerly login_from_session); pass it in session_id. session_id: PHPSESSID cookie value, required when method='session'.

Returns: dict with logged_in status, subdomain, user_id, role and whether 2FA is pending. When two_factor_required is true, finish with two_factor_finish.

Notes: - Each login call adds/replaces that subdomain's session; call login_all to log into several schools in one call. - When EDUPAGE_SUBDOMAINS is set it is a strict allowlist: login into a school outside it is refused. - Prefer setting EDUPAGE_USERNAME / EDUPAGE_PASSWORD (and EDUPAGE_SUBDOMAINS for multi-school) — the server then logs in automatically at startup.

Inputs: method, password, username, subdomain, +1 more.

login_allA
Idempotent

Log in to one or more schools in a single call. Writes: establishes (or replaces) the server-side session for each subdomain.

Args: subdomains: Comma-separated school subdomains, e.g. 'school1,school2'. Falls back to EDUPAGE_SUBDOMAINS. usernames: Comma-separated usernames, one per school (or a single one). Falls back to EDUPAGE_USERNAME. passwords: Comma-separated passwords, one per school (or a single one). Falls back to EDUPAGE_PASSWORD.

Returns: dict: {'results': [{subdomain, ok, user_id, role, two_factor_required}], 'active_subdomain': ...}. A failed school is reported per-entry with its error.

Notes: - Add schools one at a time with login. - When EDUPAGE_SUBDOMAINS is set it is a strict allowlist: schools outside it are refused per-entry without creating a session. - Finish any pending 2FA with two_factor_finish.

Inputs: passwords, usernames, subdomains.

rate_mealA
DestructiveIdempotent

Rate the taste and portion size of a meal. Writes: submits both scores and overwrites whatever rating this account's student already gave for that date and meal type.

One rating per (date, meal_type), filed against the student the session is logged in as. It replaces the previous rating outright — no history is kept and no tool here undoes it — so the call is safe to repeat with the same scores, but changing a score means submitting again and losing the old one.

Rate a meal the student actually ate. Read get_meals first to confirm the slot is published and to see the current averages. Not an ordering tool: choose_meal books a menu and sign_off_meal releases the booking, and neither records a rating. Only the three canteen slots can be rated — breakfast and dinner, which get_meals also returns, carry no rating board and fail with "No 'breakfast' meal available".

Args: date_str: YYYY-MM-DD of the meal that was eaten. meal_type: 'snack' | 'lunch' | 'afternoon_snack' — the slots choose_meal and sign_off_meal also accept, and the only ones with a rating board. quality: Taste, 1 (inedible) to 5 (excellent), integer. quantity: Portion size, 1 (far too little) to 5 (far too much), integer. Submitted in the same request as quality; neither score can be left out or rated on its own. subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'rated': True, 'meal_type', 'date'}. The scores are not echoed back — re-read get_meals to confirm what was recorded.

Notes: - Fails with "No rating available for this meal" when the school has not opened ratings for that menu, and with a "Missing boarder id" error when the session carries no canteen list to file the score against — switch_to_student first if one login covers several children. - The score posts to the school's canteen endpoint under that dining id and counts towards the averages get_meals shows, so it reaches the canteen and cannot be deleted through this server. - Upstream sends a fixed meal-slot field ("jedlo_dna" = 2, lunch) with the rating, so a snack or afternoon-snack score is recorded against the lunch entry.

Inputs: quality (required), date_str (required), quantity (required), meal_type (required), +1 more.

scan_studentsA
Read-onlyIdempotent

Discover all students visible to the logged-in account across the discovery scope (the configured EDUPAGE_SUBDOMAINS, or every school when unset). Read-only; uses cached data to avoid redundant API calls.

Args: None. This tool takes no arguments — the discovery scope is fixed by EDUPAGE_SUBDOMAINS and the logged-in role, both of which are environment/login state rather than per-call input.

Returns: dict: {'students': [{name, student_id, class_id, subdomain}], 'total': n}. For a parent account: their linked children in each school; for a student/teacher: classmates. One entry per student per school.

Notes: - get_my_students returns the same view for the active subdomain; clear_student_cache refreshes it.

Inputs: no named parameters.

send_messageA
Destructive

Send a message to a recipient. Writes: posts a new message on the recipient's timeline.

Args: recipient_id: EduPage id like 'Student123' or 'Teacher456' (see get_roster). body: Message text. Must not be empty. subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'sent': True, 'timeline_id': }.

Notes: - Recipient ids come from get_roster(roster_type='students'|'teachers') or get_my_students.

Inputs: body (required), recipient_id (required), subdomain.

sign_off_mealA
DestructiveIdempotent

Cancel an ordered meal for a date. Writes: releases the booking.

Args: date_str: YYYY-MM-DD to cancel. meal_type: 'snack' | 'lunch' | 'afternoon_snack'. subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'ordered': False, meal_type, date}.

Inputs: date_str (required), meal_type (required), subdomain.

switch_to_parentA
Idempotent

Switch the session back to the parent account (parent accounts only). Writes: changes which account subsequent tools operate as.

Args: subdomain: School whose session to restore (defaults to the active).

Returns: dict: {'switched_to_parent': True, 'user_id': ...}.

Notes: - Pair with switch_to_student; only relevant after a parent session was switched to a child.

Inputs: subdomain.

switch_to_studentA
Idempotent

Switch the session to a student account (parent accounts only). Writes: changes which account subsequent tools operate as.

Args: student_id: person_id of the child (from get_my_students). name: first/last/full name of the child, used when student_id is omitted. subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'switched_to_student': <person_id>, 'user_id': ...}.

Notes: - Revert with switch_to_parent. Prefer the stateless get_student_timetable (name/student_id) over switching when you only need a timetable.

Inputs: name, subdomain, student_id.

two_factor_finishA
Idempotent

Finish a pending 2FA login. Writes: completes the pending auth flow. Only needed after a login that returned two_factor_required: True.

Args: code: Optional email/app verification code. When given it is used directly; otherwise the device-confirmation flow is polled. subdomain: School subdomain whose pending login to complete (defaults to the active subdomain). poll_seconds: How long (seconds) to wait for approval on a device when code is not given. Defaults to 60; on timeout the caller can call two_factor_finish again later.

Returns: dict: {'confirmed': True, 'logged_in': True, subdomain, user_id, role} on success, or a pending status when the confirmation wasn't approved within the poll window.

Inputs: code, subdomain, poll_seconds.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.2/5.0

    Scored across 31 tools

    Disambiguation3/5

    Several clusters overlap substantially: get_my_timetable / get_student_timetable / get_timetable / get_next_week_timetable all return timetables, and get_my_students / scan_students / find_student / get_roster all surface student lists. The descriptions do cross-reference each other well (e.g. 'prefer X over Y'), which mitigates but does not eliminate misselection risk.

    Naming Consistency5/5

    Every tool uses snake_case with a predictable verb_noun pattern (get_*, find_student, scan_students, choose_meal, sign_off_meal, send_message, download_attachment). No mixing of conventions or vague single-word names beyond login helpers, which are still clear.

    Tool Count3/5

    At 31 tools this sits in the heavy range, though the domain (auth, timetable, grades, meals, messaging, rosters, raw API) is genuinely broad. Some redundancy exists within the timetable and student-discovery clusters, so a handful of tools could be consolidated.

    Completeness5/5

    The surface covers authentication (login, login_all, 2FA), session switching, read paths for timetable/grades/timeline/meals/rosters, write paths for meals, ratings and messages, plus attachment download, cache management, and a raw request escape hatch. Coverage is unusually thorough with few dead ends.

    Maintenance

    ActivityMaintained
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      B
      maintenance
      Connects AI agents to the École Directe API to access grades, homework, timetables, school life, messages, and documents.
      -
    • A
      license
      A
      quality
      C
      maintenance
      Lets an AI agent read and manage a family's EduPage school data, including timetables, homework, grades, messages, absences, events, and canteen meals, and also send messages to teachers or order meals.
      16
      1
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      Enables AI assistants to access Librus Synergia school data, including grades, attendance, timetables, homework, calendar, announcements, messages, formative assessments, school-year history, class-free days, and student information. Supports multiple logins and self-hosted HTTP access for agents on other machines.
      31
      742 PyPI
      15
      MIT