Skip to main content
Glama

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.


Related MCP server: sjtu-canvas-mcp

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

  1. "What announcements happened across my courses this week?" — fans out across all enrolled courses, newest first.

  2. "What assignments are due next week?" — reads the calendar, including gradable items.

  3. "Organise this semester's content into ~/NTU/y3s1/sc2002/week 8/…." — walks the course tree and downloads into any folder layout you describe.

  4. "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/messages REST 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 courseErrors entry — keep since/until within a semester.

  • read_file_content extracts text from simple documents; for large or graphical PDFs, use download_file and open the file in your client.


Authentication

The BbRouter cookie is resolved strictly in this order:

  1. NTULEARN_COOKIE environment variable

  2. Config file at <config>/ntulearn-mcp/cookie

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

  1. Log in at https://ntulearn.ntu.edu.sg.

  2. DevTools (F12) → Application → Cookies → ntulearn.ntu.edu.sg.

  3. Copy the Value of the BbRouter cookie (starts with expires:).

  4. Run ntulearn-mcp setup and choose paste, or set it in your MCP host's env block:

    { "env": { "NTULEARN_COOKIE": "expires:1234567890,id:..." } }
  5. 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 check to see the cookie source and live validity, then ntulearn-mcp setup to re-capture one.

  • MCP host lists the server but tool calls return nothing — run rust/target/release/ntulearn-mcp directly. 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_content returns "No download URL found" — that content node is not a file (a text page or tool link, say). Use download_file for 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 BbRouter can 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.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server for Canvas LMS with automatic OAuth authentication. Enables interaction with courses, assignments, grades, modules, discussions, quizzes, files, calendar, messaging, and more without manual API token management.
    2,438 npm
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables read-only access to University of Waterloo Learn and Piazza, allowing users to view courses, assignments, grades, submissions, discussions, and more through an MCP server.
    MIT