Skip to main content
Glama
README.md
# bb-mcp

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-blue)]()
[![Python](https://img.shields.io/badge/python-3.10%2B-green)]()

A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for the
**CUHK-SZ Blackboard** instance (`bb.cuhk.edu.cn`, Blackboard Learn Classic 3900).
Let your AI assistant download courseware, inspect assignments, read announcements,
and submit homework — fully headless, no browser window ever pops up.

一个给 AI 助手用的中大(港中深)Blackboard MCP 工具:课件下载、作业查看与提交、公告阅读。
全程无浏览器窗口,凭据只存本机。中文说明见下方 [中文说明](#中文说明)。

> ⚠️ For personal productivity only. Use responsibly and follow your
> institution's IT policies. Don't hammer the server; don't automate anything
> that violates academic integrity rules.

## Tools (9)

| Tool | What it does |
|---|---|
| `bb_login()` | Headless login using `cred.json` (template auto-generated on first run) |
| `bb_status()` | Session check; auto re-login once with stored credentials if expired |
| `bb_list_courses()` | List all your courses |
| `bb_list_course_files(course)` | List every file in a course's content area (newest first) |
| `bb_download_courseware(course, keyword, latest)` | Download the latest courseware to `<output dir>/<course>/` |
| `bb_list_assignments(course)` | List assignment/test entry links (with visible dates) |
| `bb_assignment_details(course, keyword, download)` | Scan all content areas for assignments: summary, due date, submission-entry status, attachment download |
| `bb_announcements(course, limit)` | Read announcements (`course="all"` scans every course) |
| `bb_submit_assignment(course, assignment, file_path, confirm)` | Submit homework — **two-phase**: dry-run by default, `confirm=true` actually submits |

Every tool auto-re-authenticates when the session expires, and tells you
explicitly to update `cred.json` when your password has changed.

## Requirements

- Python **3.10+**
- A Claude Code / MCP-compatible client
- A `bb.cuhk.edu.cn` account

## Install

```bash
git clone https://github.com/changshenhan/bb-mcp.git
cd bb-mcp

python -m venv venv
# Windows
venv\Scripts\pip install -r requirements.txt
# macOS / Linux
venv/bin/pip install -r requirements.txt

# Install Chromium (goes to Playwright's default cache)
venv\Scripts\playwright install chromium
```

`requirements.txt` pins `mcp<2` (v2 renamed `FastMCP` and breaks the import)
plus `playwright`.

## Configure

**1. Credentials** — run any tool once and `cred.json` is generated next to
`server.py`. Fill in your values:

```json
{
  "username": "your_student_id",
  "password": "your_password"
}
```

The username may include `@link.cuhk.edu.cn` / `@cuhk.edu.cn` — the suffix is
stripped automatically, same as the site's own login JS. **The file stays on
your machine and is git-ignored; never commit or share it.**

**2. Register with your MCP client** (Claude Code example, see
`.mcp.json.example`):

```json
{
  "mcpServers": {
    "bb-cuhk": {
      "command": "<absolute path to venv python>",
      "args": ["<absolute path to>/bb-mcp/server.py"],
      "env": {
        "BB_OUTPUT_DIR": "C:\\path\\to\\your\\download\\folder"
      }
    }
  }
}
```

**3. Optional environment variables**

| Variable | Default | Purpose |
|---|---|---|
| `BB_OUTPUT_DIR` | `~/Desktop/BB-Courseware` | Download root for courseware & attachments |
| `PLAYWRIGHT_BROWSERS_PATH` | Playwright default cache | Custom Chromium install location |
| `BB_BASE_URL` | `https://bb.cuhk.edu.cn` | Point at another Blackboard instance (see below) |

## Usage examples

Just talk to your assistant:

- *"Download the newest slide from my Computer Networks course"*
  → `bb_download_courseware(course="Networks", latest=1)`
- *"What assignments are due in the Graph Computing course?"*
  → `bb_assignment_details(course="Graph")`
- *"Check announcements for all courses"*
  → `bb_announcements(course="all")`
- *"Submit answer.md to the Topological Sort assignment"*
  → dry-run first, then `confirm=true`

## How the login works (for hackers)

The site's custom login page (a Blackboard `bb-SSOIntegrationDemo` building
block) obfuscates its client-side crypto. Reversed, it is simply:

```
check():        password field  ← base64(pw)
validate_form(): encoded_pw         = base64(base64(pw))
                 encoded_pw_unicode = base64(utf16le(base64(pw)))   # md5.js chrsz=16
                 password field     ← ""
POST /webapps/bb-SSOIntegrationDemo-BBLEARN/execute/authValidate/customLogin?returnUrl=…
  fields: user_id, tstring (from page), pstring, encoded_pw, encoded_pw_unicode,
          action=login, one_time_token=false, ...
```

Two quirks worth knowing if you adapt this:

- **The site rejects plain Python OpenSSL TLS handshakes** (curl works fine, so
  it is client-fingerprinting). All HTTP therefore goes through Playwright's
  `APIRequestContext` — the browser's own TLS stack — which also shares the
  cookie jar with the DOM session.
- **The failure page shows no error text at all** (302 back to `/`, clean login
  form). Login success is therefore decided by a positive check: opening the
  portal tab and verifying it does *not* bounce to `/webapps/login`.

Porting to another school: flip `BB_BASE_URL` and try. If your instance uses
the same custom-login building block it will work as-is; if it redirects to
ADFS/Shibboleth you'll need to rewrite `_http_login()` against your login
page's form fields.

## Download dedup rules

- Filename comes from `content-disposition`; when missing, the content item's
  display name + a content-type-derived extension is used.
- A file is skipped when an identical filename (case-insensitive) **or** an
  identical byte size (>1 KB) already exists under the output root — manually
  renamed copies from previous downloads are recognised by size.

## Self-test

```bash
python _mcp_selftest.py
# → handshake ok, tools: [... 9 tools ...]
```

## Known limits

- Built for Blackboard **Classic** (JSP pages). If your school switches to
  Ultra, `_collect_files` and the submission selectors need a rewrite.
- File dates are scraped from `YYYY-MM-DD` text near the content item;
  undated items sort last.
- The submission receipt is a page-text excerpt — always eyeball it after
  submitting.

## Security

- Credentials live only in `./cred.json` (git-ignored), sessions only in
  `./profile/` (git-ignored). Nothing is sent anywhere except
  `bb.cuhk.edu.cn` itself.
- `bb_submit_assignment` is two-phase by design: the default `confirm=false`
  call only opens the attempt page and returns an excerpt; nothing is
  submitted until you pass `confirm=true`.

## 中文说明

1. `pip install -r requirements.txt` 后执行 `playwright install chromium`
2. 首次运行任意工具,目录里会自动生成 `cred.json`,填入学号密码保存
   (带不带 `@link.cuhk.edu.cn` 后缀都行)
3. 把 `.mcp.json.example` 抄进你项目根的 `.mcp.json`,路径改成你的 venv
   Python 和本目录 `server.py` 的绝对路径
4. 之后对 Claude 说人话即可:「把网络课最新课件下下来」「看看有哪些作业待交」
5. 改了学校密码?工具会明确提示「登录失败」,把新密码填回 `cred.json` 再跑

下载默认落到 `~/Desktop/BB-Courseware/<课程名>/`,作业附件在 `<课程名>/assignments/`,
可用 `BB_OUTPUT_DIR` 环境变量改。

## License

[MIT](LICENSE)