ntulearn-mcp
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
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 small Rust binary with a built-in minimal MCP stdio implementation (no heavyweight framework — see the dependency diet), 22 tools, 2 prompt templates, and a course-resource URI. No Python, no pip, no virtualenv.
Website: https://ntulearn.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-mcpLog in once — setup opens NTULearn in a dedicated browser window and captures your session for
you. No copy-paste, no devtools, no keychain/password prompts. After that the server renews the
session silently in the background. (No terminal? Ask your agent to call ntulearn_login.)
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.
Related MCP server: ZHAW Moodle MCP Server
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" # concurrentMeasured with the built-in benchmark (ntulearn-mcp selfbench):
Metric | MCP | CLI | Improvement |
Discovery tokens (per conversation) | 7,155 (every turn) | 633 (once, on demand) | 11.3x |
Task output tokens (4-task suite) | 2,193 | 492 (compact TSV) | 4.5x |
10-turn conversation model | ~73,743 tok | ~1,125 tok | 65.5x |
Latency (mock API, 25 ms RTT, cold cache): all-courses tools fetch every enrolled course
concurrently (bounded at 48 in flight) instead of one-by-one. announcements across 6 courses
drops from 214 ms to 60 ms, the whats_new digest from 703 ms to 65 ms, and a 44-course whats_new
completes in ~0.10 s. Content trees walk one level per wave: a 403-node tree (13 weeks x 5 modules x
5 files) drops from 2.3 s to 0.13 s. Multi-file downloads and attachment URL resolution run in parallel too.
Latency check | Before | Now | Gain |
| - | 58ms | 3.4x vs v0.3 |
| - | 57µs | - |
4 commands concurrently ( | 225ms sequential | 60ms (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 .csvOutput 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 |
| Human-readable. |
JSON |
| Raw payload. |
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: "json" or "markdown" too — and as of v0.7.2
TOON is the default for MCP tool responses (the CLI keeps its compact-TSV default).
Tools
21 tools, most doing cross-course aggregation by default — you rarely pass course IDs by hand.
Tool | What it does |
| List enrolled courses. |
| Walk a course's content tree. Omit |
| Recursive substring search within one course. |
| Calendar items across enrolled courses. Defaults to the next 2 weeks. |
| Announcements across enrolled courses, newest first. Optional |
| Gradebook columns across enrolled courses, with your scores when available. |
| Download every file on a content item to disk. |
| Read an attached file's content inline (no filesystem hop). |
| List mailbox messages (inbox/sent). |
| Read one message by ID, with full body and recipients. |
| List users in a course (instructors, TAs, students). |
| List the groups defined in a course. |
| List the members of a course group. |
| List submission attempts for an assignment. |
| Search content across all courses; results carry courseId + breadcrumb. |
| One course's entire content tree as nested JSON (bounded by |
| Recursively download every file in a course to |
| One-call digest: announcements + upcoming + gradebook since a cutoff. |
| Export calendar items as an iCalendar |
| Export your gradebook as a CSV string. |
| Briefing for one course: instructors, upcoming, announcements, grades, top folders. |
Read-only tools default to response_format='toon' (token-optimized); pass 'json' for structured
data or '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 variableConfig file at
<config>/ntulearn-mcp/cookieFirefox
cookies.sqlite(read-only, plaintext)
No keychain access, ever — no password dialogs, no security commands. The capture browser is
launched with --use-mock-keychain --password-store=basic, so it never asks macOS Keychain, GNOME
Keyring or KWallet for anything.
setup (or the ntulearn_login tool) is the first-run path. It works with any Chromium-based
browser you have installed — Chrome, Edge, Brave, Arc, Vivaldi, Opera, Chromium — regardless of
which browser you normally use, and never touches your real browser profile. It opens NTULearn in a
dedicated profile (<config>/ntulearn-mcp/capture-profile-v2), waits for you to log in, validates
the cookie live, and saves it. Answer Yes to "Stay signed in?".
After that, sessions renew themselves:
NTULearn cookies last ~3 h. Before one expires — or when a call hits 401 — the server re-logs in through the saved NTU SSO session in a headless browser: no window, no password.
SSO session cookies are re-saved with a 30-day lifetime in the dedicated profile, so the silent re-login keeps working across browser and server restarts (NTU's IdP still enforces its own limit).
When even the SSO session has ended, the login window opens by itself in the background (only on machines that have logged in through the capture flow before — a fresh install never spawns a window unasked). The failing tool returns "a browser login window has just opened"; log in there, and a retry of the same request succeeds — the fresh session is hot-swapped in the moment it lands. At most one window is ever open, no matter how many tools fail while you are away.
Browsers never pile up: capture is single-flight machine-wide (a cross-process lock covers multiple MCP hosts and the CLI at once), orphaned capture browsers from a killed server are reaped automatically, an unattended login window is not re-opened for 30 min, and when the SSO session is dead all silent sign-in retries back off for 6 h instead of launching headless browsers that cannot succeed.
Each laptop needs its own one-time login. With no Chromium browser at all (or set
NTULEARN_BROWSER to a binary path), setup falls back to a one-time paste.
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 |
| — | Manual cookie fallback. |
| auto-detect | Path to a Chromium-based browser binary for login capture. |
|
| Change for a different Blackboard instance. |
|
| Default |
|
| SQLite response cache (falls back to in-memory). |
|
|
|
|
| Max in-flight HTTP requests. Lower it if a tenant rate-limits; raise it for large course lists. |
|
|
|
| upstream repo | Where unexpected-error issues are filed. |
| — | Set to |
| — | Set to |
| 86400 | Check throttle. |
| upstream repo | Override for forks. |
| — | Set to |
Updates
New releases auto-install by default: the binary checks GitHub at most once every 24 hours, and
when a newer release exists it is downloaded, signature-verified, and atomically swapped in — the
new version activates when your MCP host next restarts the server. The first interactive
setup/update run asks once whether you want this (answer "no" to switch to notify-only), and
update --auto off changes it any time. A one-line notice still appears when an update lands.
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 (default)
ntulearn-mcp update --channel beta # try prereleases firstntulearn-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 firstWith --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/.
Error reporting (opt-in)
The server can auto-file a GitHub issue when it keeps hitting the same unexpected error — the
kind that would otherwise only surface as a dead end in some agent's transcript. This is opt-in:
you are asked once during setup/update, or toggle it directly:
ntulearn-mcp issues on # opt in
ntulearn-mcp issues off # opt out
ntulearn-mcp issues # status: enabled, gh availability, filed countBehavior:
Only unexpected errors. Auth/session expiry, bad arguments, and transient network failures self-heal or are the caller's fault — they are never filed. Parse failures, resolution dead ends ("No download URL found"), and panics are.
Once per error per version. Errors are normalized to a signature; a signature is filed at most once per binary version (an existing open issue with the same title is detected across machines too).
Public-repo safe. Course/content IDs, emails, cookie values, and long tokens are redacted before anything leaves the machine.
Needs
gh. Filed through the GitHub CLI using your localghauth; no-op when absent.NTULEARN_ISSUE_REPOretargets (useful for forks),NTULEARN_ISSUE_REPORTS=offforce-disables.
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
"Not logged in to NTULearn", or tools fail with 401 — ask your agent to call
ntulearn_login, or runntulearn-mcp setup.ntulearn-mcp checkshows the cookie source and live validity.A macOS "wants to use your confidential information" prompt — that came from versions before the mock-keychain fix; update, click Deny, and log in once more with
setup.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" — forx-bb-documentpages this is fixed (the page body's embedded file links are now resolved). If it still happens, the content node is likely not a file at all (a text page or tool link) — and withissues onthe server files a report automatically.
A single small binary
The server has no heavyweight framework behind it: the MCP stdio transport is
~450 lines of in-tree JSON-RPC (src/mcp.rs) over tokio, reqwest does the REST
calls (rustls, gzip only), and rusqlite backs the TTL cache and Firefox cookie
reads. As of v0.7.2:
Metric | v0.7.1 | v0.7.2 |
Crates compiled | 383 | 277 |
Release binary | 16.4 MB | 5.7 MB |
Cold | minutes (brotli + ring C toolchains in the graph) | ~64 s on an M-series laptop |
selfbench confirms zero runtime regression: warm dispatch ~0.1 ms, cold
mock-RTT numbers unchanged (see "Why the CLI" above).
Development
cd rust
cargo test # 111 unit tests
cargo build --release
target/release/ntulearn-mcp selfbench # token + latency + concurrency benchmarkLayout (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 registryBefore 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.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
PLATO (Pusan National University LMS) MCP server via Moodle Web Service API + ubboard scraping
Public read-only MCP access to digital learning concepts, semantic relationships, and citations.
MCP server for the Inistate platform: module discovery, entry management, and activity submission.
Read users, courses and progress; enroll users in courses or user groups in SkyPrep.
Related MCP Servers
- FlicenseAqualityCmaintenanceEnables MCP-compatible clients to read courses, assignments, announcements, files, and grades from a Canvas LMS instance.10-
- AlicenseAqualityBmaintenanceLets MCP clients access a student's ZHAW Moodle account to list courses, find and download learning materials, sync local copies, and track deadlines, assignments, and announcements.16MIT
- AlicenseNot gradedqualityBmaintenanceEnables users to retrieve Modeus schedules, iStudent grade/rating data, and eLearn course listings, content, and files through local MCP tools while using their university service login.1Apache 2.0
- AlicenseNot gradedqualityCmaintenanceEnables users to read Moodle course information, list courses, outlines, resources, assignments, and deadlines, and download or extract text from course files locally via an MCP-compatible client.MIT