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 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-mcp

Log 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"   # concurrent

Measured 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

courses cold (2 RTTs, 25ms mock)

-

58ms

3.4x vs v0.3

courses warm (SQLite cache)

-

57µs

-

4 commands concurrently (multi)

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

  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: "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

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='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/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. 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.

  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_BROWSER

auto-detect

Path to a Chromium-based browser binary for login capture.

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_HTTP_CONCURRENCY

48

Max in-flight HTTP requests. Lower it if a tenant rate-limits; raise it for large course lists.

NTULEARN_MCP_AUTOUPDATE

on

on, off, or ask.

NTULEARN_ISSUE_REPO

upstream repo

Where unexpected-error issues are filed.

NTULEARN_ISSUE_REPORTS

—

Set to off to force-disable issue auto-filing.

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

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 first
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/.


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 count

Behavior:

  • 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 local gh auth; no-op when absent. NTULEARN_ISSUE_REPO retargets (useful for forks), NTULEARN_ISSUE_REPORTS=off force-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 run ntulearn-mcp setup. ntulearn-mcp check shows 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-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" — for x-bb-document pages 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 with issues on the 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 cargo build --release

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

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      A
      quality
      C
      maintenance
      Enables MCP-compatible clients to read courses, assignments, announcements, files, and grades from a Canvas LMS instance.
      10
      -
    • A
      license
      A
      quality
      B
      maintenance
      Lets 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.
      16
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables 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.
      1
      Apache 2.0
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables 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