polyu-estudent-mcp
README.md
# eStudent MCP
**English** | [中文](#中文文档)
A local [MCP](https://modelcontextprotocol.io) server that lets Claude Code
operate the PolyU **eStudent** portal — check grades, timetable and exams,
search subjects, and (work in progress) add/drop & snipe courses.
**Everything runs on your own machine. Your NetID and password never leave it.**
> ⚠️ This is an unofficial personal tool, not affiliated with PolyU. Use it on
> your own account and at your own risk, and respect the university's
> acceptable-use policy.
---
## ✨ Features & Status
| Tool | What it does | Status |
|---|---|---|
| `session_status` | Is there a live logged-in session? | ✅ Live-verified |
| `get_grades` | Grades & GPA, optional `term` filter | ✅ Live-verified |
| `get_timetable` | Class timetable | ✅ Live-verified |
| `get_exam_schedule` | Exam dates / venues / seats | ✅ Live-verified |
| `search_subjects` | Search by code/keyword **or** by programme; auto term, pagination, per-group vacancy on a single match | ✅ Live-verified |
| `preview_registration` | Preview add/drop — **never submits** | ⏳ Logic done; live calibration pending |
| `confirm_registration` | Submit add/drop (needs preview `fingerprint`) | ⏳ Pending registration window |
| `start_course_sniper` | Automated grab (open-time / vacancy-watch) | ⏳ Scheduler done; real submit pending |
| `sniper_status` | List / inspect / stop sniper jobs | ✅ Works |
### Done & verified
- **Login** via PolyU ADFS SSO (NetID + password, no 2FA for the tested account);
session is persisted and reused.
- **Read tools** (`get_grades` / `get_timetable` / `get_exam_schedule`) calibrated
against the live portal.
- **`search_subjects`** — both **by subject** (code/title) and **by programme**
(department → programme cascade); tries each term automatically when none is
given; walks the result pagination to collect all subjects; when the query
resolves to exactly one subject it drills into the detail page to return each
teaching group's **vacancy** (handles `5` open / `(4)` reserved / `W=.. Top-up
vac=..` waitlist).
- **Two-step registration invariant**, **sniper frequency floors**, the
sniper's **self-healing retry logic** (auto re-login, browser relaunch,
open-time → vacancy-watch fallback), and all **parsers** are unit-tested
(27 tests).
### Not finished yet
- `preview_registration` / `confirm_registration` real submission, and the
sniper's real grab path, can only be calibrated when the **subject
registration window is open** (it was closed during development). The
scheduling, frequency safety, and preview→confirm fingerprint logic are
already built and tested.
- `search_subjects` keyword results across many pages are de-duplicated but
large title searches return the offered subjects only (no extra filters yet).
---
## 🚀 Installation
**Prerequisites**: macOS, Python 3.12, [`uv`](https://docs.astral.sh/uv/),
and [Claude Code](https://claude.com/claude-code).
```bash
# 1. Clone
git clone https://github.com/<your-account>/polyu-estudent-mcp.git
cd polyu-estudent-mcp
# 2. Create the venv and install deps
uv sync --extra dev
# 3. Install the headless browser Playwright drives
.venv/bin/python -m playwright install chromium
# 4. Configure your credentials
cp .env.example .env
# then edit .env — see the next section
```
### Configure `.env`
`.env` is **git-ignored** and must contain your real credentials. It never gets
committed or pushed.
```ini
ESTUDENT_NETID=your_netid # e.g. 12345678d
ESTUDENT_PASSWORD=your_password # your portal password
ESTUDENT_BASE_URL=https://www.polyu.edu.hk/student
ESTUDENT_HEADFUL=0 # 1 = show the browser (debugging)
```
```bash
chmod 600 .env # restrict to your user
```
### Verify
```bash
uv run pytest -q # 27 tests should pass
```
---
## 🔌 Register with Claude Code
```bash
claude mcp add estudent -s user -- \
"$(pwd)/.venv/bin/python" -m estudent_mcp.server
```
`-s user` registers it globally (available in every project). Drop it for the
current project only. Confirm it connected:
```bash
claude mcp get estudent # should show ✔ Connected
```
---
## 💬 Usage
Just talk to Claude Code in natural language — it picks the right tool.
| You say | Tool used |
|---|---|
| "What's my GPA?" | `get_grades` |
| "Show my timetable" | `get_timetable` |
| "When are my exams?" | `get_exam_schedule` |
| "Find COMP1011 and its vacancy" | `search_subjects` |
| "Search programmes in COMP for BA(HONS) COMPUTING" | `search_subjects` (by programme) |
| "Watch COMP2012 for a vacancy" | `start_course_sniper` (⏳) |
### Two-step registration
`confirm_registration` only submits if you pass the `fingerprint` returned by a
`preview_registration` of the **same** actions — so you can never add/drop a
subject without first seeing exactly what will change.
### The sniper is deliberately restrained
High-frequency "bombing" polling is **not supported**, by design — it's the
fastest way to get rate-limited/banned (so you'd miss the course anyway), may
violate the acceptable-use policy, and is unfair to others.
Frequency floors are enforced (sub-floor configs are rejected):
- **open_time** mode: retry ≥ 3s, intense window ≤ 30 min.
- **watch_vacancy** mode: poll ≥ 30s.
Note the real rate limiter is the attempt itself — each one is a full browser
flow taking several seconds — so "every 3s" works out to roughly 4–6 attempts
per minute, comparable to a fast human refreshing.
Within those floors, jobs are **self-healing** and built to survive the
launch-day crush: during the open window page timeouts shrink to ~12s so a
crashed portal costs seconds per probe, an expired session re-authenticates
automatically with your `.env` credentials, and a crashed browser is closed
and relaunched. Errors are classified: *portal unreachable* (timeouts,
connection errors — expected while the portal is being hammered) is judged by
duration and only ends the job after 30 minutes of continuous downtime;
*page-structure* errors (portal redesign) end it after 5 strikes; rejected
credentials end it immediately. Pass `then_watch=true` to *open_time* so an
unsuccessful open window falls through to vacancy watching instead of giving
up. Terminal events (grabbed / fallback / failed) raise a native macOS
notification.
Keep your Mac awake while a job runs — e.g. `caffeinate -dims` in a spare
terminal. Set `ESTUDENT_HEADFUL=1` to watch the browser.
---
## 🔒 Security
- `.env` (your credentials), `.runtime/` (saved session + any captured page
HTML, which may contain personal data), and screenshots are **all git-ignored**
and were **never committed** to history.
- Only `.env.example` (a placeholder template) is tracked.
- Credentials are read locally by `config.py` and used solely to log into
eStudent from your machine.
If you fork/clone, never commit your `.env`.
---
## 🏗 Architecture
```
src/estudent_mcp/
server.py # MCP tool layer (no browser code)
backend/
base.py # EStudentBackend interface (stable)
playwright_backend.py # scheme A: headless browser (current)
# future: hybrid_backend.py — scheme C: Playwright login + httpx fetch
parsers/ # HTML -> dataclasses (pure, unit-tested)
registration.py # fingerprint + summary (two-step invariant)
sniper.py # scheduler + frequency floors
models.py config.py errors.py
tests/ # parser / registration / sniper unit tests
scripts/ # joint-debug probes (no PII committed)
```
The `EStudentBackend` interface lets a faster HTTP backend (scheme C) be swapped
in later without touching the tools or parsers.
---
## 🛠 Why a browser instead of an API?
eStudent exposes **no public API**, so the backend drives a real headless
Chromium (via Playwright) that logs in and reads pages exactly like a human
would, then parses the HTML tables into structured data.
It passes login because PolyU's ADFS presented no CAPTCHA/2FA for the tested
account and a real browser is used — **not** by defeating any verification. If
the school adds a CAPTCHA/2FA, the right move is headful mode (`ESTUDENT_HEADFUL=1`)
to solve it once and reuse the session.
---
## 📄 License
Personal use. No warranty.
<br>
---
---
<br>
# 中文文档
[English](#estudent-mcp) | **中文**
一个本地 [MCP](https://modelcontextprotocol.io) 服务器,让 Claude Code 帮你操作
香港理工大学 **eStudent** 门户:查成绩、课表、考试,搜课,以及(开发中)选课/退课
与抢课。
**所有操作都在你本机运行,账号密码绝不外传。**
> ⚠️ 本项目为非官方个人工具,与理大无关。仅用于你自己的账号,风险自负,并请遵守学校的
> 网络使用规定。
---
## ✨ 功能与完成度
| 工具 | 功能 | 状态 |
|---|---|---|
| `session_status` | 是否有有效登录会话 | ✅ 已实测 |
| `get_grades` | 成绩与 GPA,可按学期过滤 | ✅ 已实测 |
| `get_timetable` | 上课时间表 | ✅ 已实测 |
| `get_exam_schedule` | 考试日期、地点、座位 | ✅ 已实测 |
| `search_subjects` | 按代码/关键词、或按专业搜课;自动选学期、自动翻页、单科目下钻名额 | ✅ 已实测 |
| `preview_registration` | 预演选退课,**不提交** | ⏳ 逻辑完成,待联调 |
| `confirm_registration` | 提交选退课(需预演指纹) | ⏳ 待选课窗口开放 |
| `start_course_sniper` | 定时抢课(开放瞬间 / 满员捡漏) | ⏳ 调度完成,真实提交待接通 |
| `sniper_status` | 查看、检查、停止抢课任务 | ✅ 可用 |
### 已完成并验证
- **登录**走理大 ADFS 单点登录(账号密码,测试账号无 2FA),会话持久化复用。
- **读取类工具**(`get_grades` / `get_timetable` / `get_exam_schedule`)已对真实门户校准。
- **`search_subjects`** 支持**按科目**(代码/标题)和**按专业**(院系 → 专业级联)两种
模式;无学期时自动逐学期尝试;自动翻页收齐全部科目;命中单个科目时下钻详情页返回每个
教学组的**名额**(识别 `5` 开放 / `(4)` 保留 / `W=.. Top-up vac=..` 候补)。
- **两步选课确认**、**抢课频率下限**、抢课**自愈重试逻辑**(自动重登、浏览器重启、
开抢失败转捡漏)、所有**解析器**均有单测(27 个)。
### 尚未完成
- 选课/退课的真实提交(`preview_registration` / `confirm_registration`)、抢课真实下单,
必须等**选课窗口开放**才能联调(开发期间未开放)。调度、频率保护、预演→确认指纹逻辑
已写好并测试。
- 关键词大范围搜索已去重,但暂未加更多筛选条件。
---
## 🚀 安装
**前置要求**:macOS、Python 3.12、[`uv`](https://docs.astral.sh/uv/)、
[Claude Code](https://claude.com/claude-code)。
```bash
# 1. 克隆
git clone https://github.com/<your-account>/polyu-estudent-mcp.git
cd polyu-estudent-mcp
# 2. 创建虚拟环境并装依赖
uv sync --extra dev
# 3. 安装 Playwright 用的浏览器
.venv/bin/python -m playwright install chromium
# 4. 配置凭据
cp .env.example .env
# 然后编辑 .env —— 见下一节
```
### 配置 `.env`
`.env` **已被 git 忽略**,填你的真实凭据,绝不会被提交或推送。
```ini
ESTUDENT_NETID=your_netid # 例如 12345678d
ESTUDENT_PASSWORD=your_password # 门户密码
ESTUDENT_BASE_URL=https://www.polyu.edu.hk/student
ESTUDENT_HEADFUL=0 # 1 = 显示浏览器(调试用)
```
```bash
chmod 600 .env # 仅本人可读写
```
### 验证
```bash
uv run pytest -q # 应有 27 个测试通过
```
---
## 🔌 注册到 Claude Code
```bash
claude mcp add estudent -s user -- \
"$(pwd)/.venv/bin/python" -m estudent_mcp.server
```
`-s user` 是全局注册(所有项目可用),去掉则只对当前项目生效。确认连接:
```bash
claude mcp get estudent # 应显示 ✔ Connected
```
---
## 💬 使用
直接用自然语言跟 Claude Code 说,它会自动选用合适的工具。
| 你说 | 用到的工具 |
|---|---|
| "查一下我的成绩和 GPA" | `get_grades` |
| "这学期课表" | `get_timetable` |
| "考试安排" | `get_exam_schedule` |
| "搜一下 COMP1011 还有没有名额" | `search_subjects` |
| "按专业搜 COMP 系 BA(HONS) COMPUTING 的课" | `search_subjects`(按专业) |
| "帮我盯着 COMP2012 有没有人退课" | `start_course_sniper`(⏳) |
### 两步选课确认
`confirm_registration` 只有在你回传**同一组操作**的 `preview_registration` 指纹时才会
真正提交——确保你在提交前一定先看清会发生什么。
### 抢课刻意克制
高频"轰炸式"轮询**不支持**:那是被限速/封号最快的方式(反而抢不到),可能违反学校规定,
也对他人不公平。
强制频率下限(低于下限直接拒绝):
- **open_time** 模式:重试间隔 ≥ 3 秒,密集抢课窗口 ≤ 30 分钟。
- **watch_vacancy** 模式:轮询间隔 ≥ 30 秒。
注意真正的限速器是尝试本身——每次都是一整套浏览器操作、耗时数秒,所以"每 3 秒
一次"实际约为每分钟 4~6 次,和一个手快的人不停刷新相当。
在下限之内,任务具备**自愈能力**,专为开抢瞬间系统被挤崩的场景设计:开抢窗口
内页面超时压缩到约 12 秒(崩溃时每次探测只花几秒而不是干等 45 秒);登录过期
自动用 `.env` 凭据重登;浏览器崩溃自动关闭重启。错误分类处理:*门户不可达*
(超时、连接错误——开抢拥挤期的常态)按持续时长判定,连续宕机 30 分钟才放弃;
*页面结构错误*(门户改版)连续 5 次终止;密码被拒立即终止。*open_time* 模式可传
`then_watch=true`:开抢窗口没抢到时自动转入捡漏轮询而不是放弃。
抢到 / 转捡漏 / 失败等终态会弹出 macOS 系统通知。
任务运行时让 Mac 保持唤醒——可在另一个终端跑 `caffeinate -dims`。
设 `ESTUDENT_HEADFUL=1` 可看到浏览器。
---
## 🔒 安全
- `.env`(凭据)、`.runtime/`(会话与抓取的页面 HTML,可能含个人数据)、截图**全部被
git 忽略**,且**从未进入提交历史**。
- 仓库里只跟踪 `.env.example`(占位模板)。
- 凭据由 `config.py` 在本地读取,只用于从你本机登录 eStudent。
如果你 fork/clone,切勿提交你的 `.env`。
---
## 🏗 架构
```
src/estudent_mcp/
server.py # 工具层,不碰浏览器
backend/
base.py # EStudentBackend 稳定接口
playwright_backend.py # 方案A:无头浏览器(当前)
# future: hybrid_backend.py — 方案C:Playwright 登录 + httpx 取数(预留)
parsers/ # HTML -> dataclasses,纯函数可单测
registration.py # 指纹 + 摘要(两步确认)
sniper.py # 调度 + 频率下限
models.py config.py errors.py
tests/ # 解析/选课/抢课单元测试
scripts/ # 联调探测脚本(不含个人数据)
```
`EStudentBackend` 接口让以后可无痛换成更快的 HTTP 后端(方案 C),不动工具层和解析层。
---
## 🛠 为什么用浏览器而非 API?
eStudent **没有公开 API**,所以后端用 Playwright 驱动一个真实的无头 Chromium,像人一样
登录、读页面,再把 HTML 表格解析成结构化数据。
它能登录,是因为测试账号的 ADFS 没有验证码/2FA、且用的是真实浏览器——**不是**靠破解
验证。若学校加了验证码/2FA,正确做法是用有头模式(`ESTUDENT_HEADFUL=1`)手动过一次、
复用会话。
---
## 📄 许可
个人使用,不提供任何担保。
TDQS
A3.8/5.0
Scored across 9 tools
Disambiguation5/5
Each tool serves a distinct purpose: registration preview/confirm, viewing timetables/exams/grades, searching subjects, checking session status, and managing sniper jobs. No overlap in functionality.
Naming Consistency3/5
Most tools use verb_noun pattern (e.g., get_exam_schedule, start_course_sniper), but 'session_status' and 'sniper_status' are noun phrases without a verb, creating inconsistency.
Tool Count5/5
With 9 tools covering registration, schedules, grades, search, and automated grabbing, the count is well-suited to the domain without being overwhelming.
Completeness5/5
The toolset covers the full registration lifecycle (preview and confirm), all key views (timetable, exam schedule, grades), search, and even automated sniper management. No obvious gaps.
Maintenance
ActivityMaintained
ResponsivenessSyncing