About Ntulearn MCP
Ntulearn MCP is an MCP server published by gangula-karthik in the Documentation category: mCP server for NTULearn (NTU Blackboard): read-only course/announcement/calendar/grade tools + course file download. Fast: HTTP/2, caching, 21 tools.
The repository has 5 stars and 1 forks, with the last commit a month ago. Recent commit activity is the strongest signal that reported issues will get attention.
Install
git clone https://github.com/gangula-karthik/ntulearn-mcpThis server has no ConduID identity, so agent calls to it are not receipted. Pin the version you install and review the source before granting it credentials.
Tools (21)
ntulearn_download_courseRecursively download every file in a course to `~/Downloads/NTU/<course>`.
Parameters not published.
ntulearn_download_fileDownload every file on a content item to disk. `destination_dir` builds hierarchies.
Parameters not published.
ntulearn_export_calendar_icsExport calendar items as an iCalendar `.ics` string.
Parameters not published.
ntulearn_export_gradebook_csvExport your gradebook as a CSV string.
Parameters not published.
ntulearn_get_announcementsAnnouncements across enrolled courses, newest first. Optional `since`.
Parameters not published.
ntulearn_get_content_treeOne course's entire content tree as nested JSON (bounded by `max_depth`).
Parameters not published.
ntulearn_get_course_contentsWalk a course's content tree. Omit `parent_id` for the top level; pass it to drill into a folder.
Parameters not published.
ntulearn_get_gradebookGradebook columns across enrolled courses, with your scores when available.
Parameters not published.
ntulearn_get_gradebook_attemptsList submission attempts for an assignment.
Parameters not published.
ntulearn_get_group_membersList the members of a course group.
Parameters not published.
Show all 21 tools
ntulearn_get_upcomingCalendar items across enrolled courses. Defaults to the next 2 weeks. `type='GradebookColumn'` filters to assignments.
Parameters not published.
ntulearn_list_course_groupsList the groups defined in a course.
Parameters not published.
ntulearn_list_course_usersList users in a course (instructors, TAs, students).
Parameters not published.
ntulearn_list_coursesList enrolled courses.
Parameters not published.
ntulearn_list_messagesList mailbox messages (inbox/sent).
Parameters not published.
ntulearn_read_file_contentRead an attached file's content inline (no filesystem hop).
Parameters not published.
ntulearn_read_messageRead one message by ID, with full body and recipients.
Parameters not published.
ntulearn_search_all_coursesSearch content across **all** courses; results carry courseId + breadcrumb.
Parameters not published.
ntulearn_search_course_contentRecursive substring search within one course.
Parameters not published.
ntulearn_summarize_courseBriefing for one course: instructors, upcoming, announcements, grades, top folders.
Parameters not published.
ntulearn_whats_newOne-call digest: announcements + upcoming + gradebook since a cutoff.
Parameters not published.
Ask AI
Ask AI about Ntulearn MCP
Powered by Claude · Grounded in docs
Security checks
Static checks derived from the repository 4 days ago. Heuristics on the source, not an audit.
- ✓README presentThe repository ships documentation.
- ✓License declaredUsable under a stated license.
- !Tests presentNo tests found. Behaviour changes may go unnoticed between versions.
- ✓Dependencies pinnedA lockfile pins dependency versions.
- ✓No dynamic code executionSource scan found no eval, dynamic Function, or shell execution.
1 check has not been run yet.
Releases
README
ntulearn-mcp
MCP server + agent-first CLI for NTULearn — NTU Singapore's Blackboard Learn instance. Point any MCP host (Claude Desktop, Claude Code, Cursor, Cline, Prime Agent) at it to ask about your courses, announcements, calendar, and grades — and to organise course files into a folder hierarchy on disk.
The shipped server is a single Rust binary built on
ultrafast-mcp, with 21 tools, 2 prompt templates, and
a course-resource URI. No Python, no pip, no virtualenv.
Website: https://ntulearn-mcp.krtk.app
Quick start
git clone https://github.com/gangula-karthik/ntulearn-mcp.git
cd ntulearn-mcp/rust
cargo build --release # binary: rust/target/release/ntulearn-mcp
Log in once — setup opens NTULearn in a throwaway-profile browser and captures your session cookie
for you. No copy-paste, no devtools, no keychain prompts.
rust/target/release/ntulearn-mcp setup
rust/target/release/ntulearn-mcp check # expect: Live validity : OK (200)
Register the binary with your MCP host:
{
"mcpServers": {
"ntulearn": {
"command": "/Users/you/ntulearn-mcp/rust/target/release/ntulearn-mcp",
"args": []
}
}
}
That's it. The same binary serves MCP with no arguments and acts as a CLI with a command.
Why the CLI: about 64x fewer tokens
MCP hosts inject every tool's JSON schema into context every turn — about 6.6k tokens for this server's 21 tools. The same binary doubles as a plain CLI, whose terse tabular output an agent reads in a few hundred tokens:
ntulearn-mcp courses # 6 courses ≈ 100 tokens (MCP tool result: ≈ 450)
ntulearn-mcp tree _2706844_1 # a course's content tree
ntulearn-mcp whats-new --days 7 # announcements + due dates + grades digest
ntulearn-mcp multi "courses" "grades" "upcoming --type GradebookColumn" # concurrent
Measured with the built-in benchmark (ntulearn-mcp selfbench):
| Metric | MCP | CLI | Improvement |
|---|---|---|---|
| Discovery tokens (per conversation) | 6,639 (every turn) | 586 (once, on demand) | 11.3x |
| Task output tokens (4-task suite) | 2,193 | 492 (compact TSV) | 4.5x |
| 10-turn conversation model | ~68,583 tok | ~1,078 tok | 63.6x |
courses cold (2 RTTs, 25ms mock) |
— | 59ms | 3.4x vs v0.3 |
courses warm (SQLite cache) |
— | 82µs | — |
4 commands concurrently (multi) |
225ms sequential | 59ms (singleflight-deduped) | 3.8x |
The CLI shares the same handlers, cache, and cookie lifecycle as the MCP server. Point your agent at
the binary and have it run ntulearn-mcp help (≈590 tokens, once) instead of registering 21 tools.
What people ask for
- "What announcements happened across my courses this week?" — fans out across all enrolled courses, newest first.
- "What assignments are due next week?" — reads the calendar, including gradable items.
- "Organise this semester's content into
~/NTU/y3s1/sc2002/week 8/…." — walks the course tree and downloads into any folder layout you describe. - "Pull the due dates and grade weightages out of this course briefing PDF." — reads small text-heavy documents inline.
For multi-page, diagram-heavy lecture decks, use download_file and open the PDF in your client —
MCP tool results are capped at 1 MB, so this server does not compete with drag-and-drop for full decks.
CLI reference
ntulearn-mcp help prints this list; ntulearn-mcp help <cmd> shows one command's flags.
courses my enrolled courses (run this first for course ids)
contents course-id one level of a course's content tree
tree course-id entire content tree of a course in one call
search course-id query search one course's content tree
find query search content across ALL courses
dl course-id content-id download a content item's files to disk
read course-id content-id read a content item's files inline (pdf/office/text)
upcoming calendar items + due dates across courses
announcements announcements across courses, newest first
grades gradebook columns + your grades across courses
attempts course-id column-id attempts on one gradebook column
messages course messages (inbox/sent)
message message-id read one course message
users course-id users enrolled in a course
groups course-id groups in a course
members course-id group-id members of one group
summary course-id one-shot course digest (staff, contents, dates)
whats-new curated digest of everything new since a cutoff
dl-course course-id bulk-download a course's files
ics export calendar items to an .ics file
grades-csv export gradebook to .csv
Output is compact TSV by default. Flags: --json, --md, --toon, --fields a,b,
--max-chars N, --full, --limit N, --offset N. Every MCP tool argument is also accepted as a
kebab-case flag (--include-disabled, --course-ids a,b, --mode vision).
multi runs several commands concurrently in one process, deduplicating identical in-flight HTTP
fetches and sharing one cache:
ntulearn-mcp multi "courses" "announcements --since 2026-01-01" "grades"
# results arrive concatenated, each prefixed with: ===== <command> =====
Output formats
| Format | Flag | Notes |
|---|---|---|
| Compact TSV | default | Smallest; field-capped, no nesting. |
| Markdown | --md |
Human-readable. |
| JSON | --json |
Raw payload. |
| TOON | --toon |
Smallest full-fidelity format — 40% fewer tokens than pretty JSON (52% on large lists). |
TOON (Token-Oriented Object Notation, spec v4.1) declares
array shapes once and drops repeated keys, so the savings scale with how tabular the payload is.
Every MCP tool accepts response_format: "toon" too.
Tools
21 tools, most doing cross-course aggregation by default — you rarely pass course IDs by hand.
| Tool | What it does |
|---|---|
ntulearn_list_courses |
List enrolled courses. |
ntulearn_get_course_contents |
Walk a course's content tree. Omit parent_id for the top level; pass it to drill into a folder. |
ntulearn_search_course_content |
Recursive substring search within one course. |
ntulearn_get_upcoming |
Calendar items across enrolled courses. Defaults to the next 2 weeks. type='GradebookColumn' filters to assignments. |
ntulearn_get_announcements |
Announcements across enrolled courses, newest first. Optional since. |
ntulearn_get_gradebook |
Gradebook columns across enrolled courses, with your scores when available. |
ntulearn_download_file |
Download every file on a content item to disk. destination_dir builds hierarchies. |
ntulearn_read_file_content |
Read an attached file's content inline (no filesystem hop). |
ntulearn_list_messages |
List mailbox messages (inbox/sent). |
ntulearn_read_message |
Read one message by ID, with full body and recipients. |
ntulearn_list_course_users |
List users in a course (instructors, TAs, students). |
ntulearn_list_course_groups |
List the groups defined in a course. |
ntulearn_get_group_members |
List the members of a course group. |
ntulearn_get_gradebook_attempts |
List submission attempts for an assignment. |
ntulearn_search_all_courses |
Search content across all courses; results carry courseId + breadcrumb. |
ntulearn_get_content_tree |
One course's entire content tree as nested JSON (bounded by max_depth). |
ntulearn_download_course |
Recursively download every file in a course to ~/Downloads/NTU/<course>. |
ntulearn_whats_new |
One-call digest: announcements + upcoming + gradebook since a cutoff. |
ntulearn_export_calendar_ics |
Export calendar items as an iCalendar .ics string. |
ntulearn_export_gradebook_csv |
Export your gradebook as a CSV string. |
ntulearn_summarize_course |
Briefing for one course: instructors, upcoming, announcements, grades, top folders. |
Read-only tools default to response_format='json'; pass 'markdown' for a readable summary.
List-returning tools accept limit/offset. A resource URI,
ntulearn://courses/{course_id}, returns a JSON course briefing, and two prompts
(ntulearn-weekly-brief, ntulearn-assignment-triage) chain the tools for you.
Known environment limits
list_messages/read_message— the public/users/me/messagesREST API returns 404 on this instance, so the client walks the internal v1 conversation API instead (one cached mailbox flatten → per-course conversations) and derives recipients from the conversation.get_group_members— the public groups endpoint returns 403 for student accounts, so the client uses the internal v1 memberships endpoint instead.- Calendar windows wider than ~16 weeks are rejected by NTULearn with a 400
courseErrorsentry — keepsince/untilwithin a semester. read_file_contentextracts text from simple documents; for large or graphical PDFs, usedownload_fileand open the file in your client.
Authentication
The BbRouter cookie is resolved strictly in this order:
NTULEARN_COOKIEenvironment variable- Config file at
<config>/ntulearn-mcp/cookie - Firefox
cookies.sqlite(read-only, plaintext)
No keychain access, ever — no password dialogs, no security commands. When NTULearn rejects a
call with 401, the server re-resolves the cookie, persists a working value, and retries once.
Refresh is never proactive; it happens on a live 401 or when you run ntulearn-mcp refresh.
setup is the normal first-run path: it validates any existing cookie live, and if there is none,
opens Chrome/Arc/Brave/Edge/Chromium with a throwaway profile and polls the DevTools protocol for
your cookie (15-minute login timeout). The captured value is validated against the API before it is
saved, then the browser and profile are cleaned up. On a headless server with no supported browser,
it falls back to a one-time paste.
The cookie expires with your NTULearn session (days to weeks). Re-run setup when it does.
-
Log in at https://ntulearn.ntu.edu.sg.
-
DevTools (
F12) → Application → Cookies →ntulearn.ntu.edu.sg. -
Copy the Value of the
BbRoutercookie (starts withexpires:). -
Run
ntulearn-mcp setupand choose paste, or set it in your MCP host'senvblock:{ "env": { "NTULEARN_COOKIE": "expires:1234567890,id:..." } } -
Restart your MCP host.
Configuration
All optional — the defaults work for most users.
| Env var | Default | Purpose |
|---|---|---|
NTULEARN_COOKIE |
— | Manual cookie fallback. |
NTULEARN_BASE_URL |
https://ntulearn.ntu.edu.sg |
Change for a different Blackboard instance. |
NTULEARN_DOWNLOAD_DIR |
./downloads |
Default destination_dir for download_file / download_course. |
NTULEARN_CACHE_DIR |
~/.cache/ntulearn-mcp/cache.sqlite3 |
SQLite response cache (falls back to in-memory). |
NTULEARN_CACHE_MODE |
readwrite |
readwrite, readonly, or off. |
NTULEARN_MCP_AUTOUPDATE |
ask |
on, off, or ask. |
NTULEARN_MCP_NO_UPDATE_CHECK |
— | Set to 1 to disable update checks. |
NTULEARN_MCP_UPDATE_INTERVAL_SECS |
86400 | Check throttle. |
NTULEARN_MCP_REPO |
upstream repo | Override for forks. |
NTULEARN_MCP_REQUIRE_SIGNATURE |
— | Set to 1 to refuse unsigned releases. |
Updates
The binary checks GitHub for a newer release at most once every 24 hours, and never blocks or breaks the server. When one is available you get a one-line notice in the MCP host's log and at the start of the session.
ntulearn-mcp update # check + prompt before installing
ntulearn-mcp update --check # exit 0 = current, 10 = update available
ntulearn-mcp update --yes # install without prompting
ntulearn-mcp update --auto on # background-install new releases
ntulearn-mcp update --channel beta # try prereleases first
With --auto on the server downloads the release in the background, verifies its sha256 against
the release's checksums.txt, and atomically swaps the binary — the new version activates when
your MCP host next restarts the server. Every checksums.txt is minisign-signed by CI, and the
public key is committed as minisign.pub and baked into the binary, so a tampered
release is refused. Set require_signature: true in settings.json to refuse unsigned releases outright. Installs the binary did not make itself (cargo, Homebrew, dev builds) are
detected and answered with the right update command instead of a self-replace. A Homebrew formula
template lives in packaging/homebrew/.
Cutting a release (maintainers): bump rust/Cargo.toml, tag v<version>, push. The workflow
builds Linux, macOS (arm64 + x64), and Windows binaries, and publishes checksums.txt with its
minisign signature. Tags with a hyphen (e.g. v0.8.0-rc.1) publish as prereleases automatically.
Troubleshooting
- "No NTULearn cookie found", or tools fail with 401 — run
ntulearn-mcp checkto see the cookie source and live validity, thenntulearn-mcp setupto re-capture one. - MCP host lists the server but tool calls return nothing — run
rust/target/release/ntulearn-mcpdirectly. The usual cause is an unresolvable cookie; the error message will say so. - Prime Agent lists the server but tools aren't available — run
/reload(or restart it) so settings are re-read. read_file_contentreturns "No download URL found" — that content node is not a file (a text page or tool link, say). Usedownload_filefor real attached files.
Development
cd rust
cargo test # 53 unit tests
cargo build --release
target/release/ntulearn-mcp selfbench # token + latency + concurrency benchmark
Layout (rust/crates/ntulearn-mcp/src/):
main.rs no args → MCP stdio server; command → CLI; setup/check/refresh
cli.rs the 21 commands, compact TSV renderer, `multi` concurrency
handlers.rs the 21 tool handlers (shared by MCP server and CLI)
bench.rs selfbench + mock Blackboard API
client.rs reqwest (HTTP/2, retries, parallel fan-out, singleflight dedup)
cache.rs SQLite (WAL, persistent conn) + in-memory TTL cache
cookie.rs layered cookie resolution (never keychain)
setup.rs setup / check / refresh subcommands
capture.rs throwaway-browser CDP cookie capture
parsers.rs HTML body → download URL extraction
render.rs markdown / csv / ics renderers
resources.rs course resource template + reader
prompts.rs prompt templates
schemas.rs tool schemas
tools.rs tool definition registry
Before pushing, check that no credential is staged or in history:
git status --short
git log --all -p | grep -nE '(BbRouter=|expires:[0-9]{10,},id:|Set-Cookie|ghp_[A-Za-z0-9]{20,})' | grep -viE 'test|example|README' || echo "clean"
If anything real shows up, do not push. Rotate the credential first.
Disclaimer
Use at your own risk. This is an unofficial, personal-use tool. It is not affiliated with, endorsed by, or sponsored by NTU Singapore, Anthology Inc., or Blackboard Learn.
- Your account, your responsibility. Driving the LMS with your session cookie may be inconsistent with NTU's acceptable use policy. Check NTU policy if you are unsure.
- Your cookie stays local. It is read locally and sent only to
ntulearn.ntu.edu.sg. - Don't share cookie values. Anyone with your
BbRoutercan act as you until it expires. - Don't run this for someone else. Each user should run their own instance against their own account.
MIT licensed — see LICENSE. All warranties are disclaimed.
README fetched from the repository 4 days ago. The original is authoritative.