BrightspaceMCP
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@BrightspaceMCPwhat's due this week in all my courses?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Brightspace-MCP
An MCP server that can expose your Brightspace account as tools available for an LLM to call.
Some assignments confound me in format and feed because every professor has a different way of assigning things, being a quiz, a content item, something on the course calendar, or even something else. Each feed is individually incomplete when calling the API so there's availability to call batch functions for multi entry retrieval.
Tools
This section was written by Claude from the tool docstrings in
server.py.
All course-scoped tools take an orgid (the OrgUnit ID) from getClasses.
getBatch* variants take a list of orgids — a real array, a JSON-array string,
or a comma-separated string, since some MCP clients stringify array args — and
fan out across courses concurrently.
Account
getUser— the calling user's profile (whoami).getClasses— current enrollments. Passfalsefor a de-duped, current-term-only list (id/name/code); passtruefor the raw D2L enrollments feed including past and hidden courses. Preferfalseto save tokens.
Grades
getAssignedGrades— the user's grade values for one course.getAllGrades— the full grade objects (definitions + values) for one course.
What's due
getWeeklyTodo/getBatchWeeklyTodo— content-linked items due in the nextdays(default 7). Each item hasItemName,DueDate,ItemUrl.getAllDueItems/getBatchAllDueItems— content-linked items with no date window (past-due through everything upcoming).getWeeklyCalendarEvents/getBatchCalendarEvents— course calendar events in the nextdays(default 7), each with aCalendarEventViewUrl.getQuizzes/getBatchQuizzes— every quiz in the Quizzes tool, whether or not an instructor linked it into content. Inactive quizzes are hidden unlessinclude_inactive=true.getEverythingDue/getBatchEverythingDue— the merged content + quizzes + calendar picture, deduped byGradeItemId(then normalized title). Each entry carries aSourceslist naming the feeds it came from.days(default 180) bounds only the calendar window. Use this when nothing can be missed.
Content
getCourseToc— the course table of contents. Default is a flat list of topics (module path,Type,Url,TopicId,GradeItemId, availability fields);full=truereturns the raw nested D2L tree. The TOC has no due dates or completion state.getTopicFile— downloads the file backing a File-type content topic (GET /le/1.82/{orgid}/content/topics/{topicId}/file). Takesorgidplus atopicIdfromgetCourseToc(only works where the topic'sTypeis a file, not a link/URL topic). ReturnsContentType/FileName/Size, thenTextfor HTML/text/XML/JSON/CSV payloads orBase64for binary ones (PDFs, slide decks, images). Redirects to D2L's signed storage URLs are followed.getSyllabus— the course syllabus / content overview (GET /le/1.82/{orgid}/overview). ReturnsDescription(rich text) andHasAttachment; when an attachment exists it is downloaded too and returned underAttachmentin the sameContentType/FileName/Size/TextorBase64shape asgetTopicFile. Passinclude_attachment=falsefor just the metadata. Not every course uses the overview — if it comes back empty, look for a "Syllabus" File topic viagetCourseToc+getTopicFile.
Completed content items and inactive quizzes are filtered out by default;
include_completed / include_inactive bring them back. Calendar events carry
no completion state, so a calendar-only entry can't be filtered that way —
cross-check getAssignedGrades for submission/score status.
On any upstream failure a tool returns {"error": ..., "endpoint": ...} in
place of its normal payload (rather than a bare null); a 401/403 — usually
expired session cookies — adds a "hint" that says so.
getLink (Kaltura lecture-video transcription) is registered only when the
optional transcription extra is installed; getLTILink (LTI quicklink
redirects) is checked in but commented out. See Optional tools.
Related MCP server: Canvas Assignment Assistant
Architecture
Claude / MCP client
│ HTTPS + Authorization: Bearer <token>
▼
nginx ($MCP_PUBLIC_HOST, TLS termination) ← My setup, http streamable claude requires HTTPS so I used Cloudflare
│ HTTP, Host preserved
▼
brightspacemcp (streamable-http, 127.0.0.1:8008) ← this repo
│ session cookies + browser UA
▼
purdue.brightspace.com/d2l/apiTransport:
streamable-httpbound to loopback.TransportSecuritySettingspinsallowed_hosts/allowed_origins(fromMCP_PUBLIC_HOST, defaultmcp.xennick.com) so the SDK's DNS-rebinding protection accepts theHostnginx forwards.Inbound auth (MCP client → this server): the
RequireTokenmiddleware rejects any request withoutAuthorization: Bearer <MCP_INBOUND_TOKEN>(constant-time compare); the token lives in.envand is minted bymint_token.py.Outbound auth (this server → Brightspace):
auth.return_cookies()readsd2lSessionVal/d2lSecureSessionValfrom.env(viapython-dotenv). Those are a logged-in browser session's cookies; a separate process is expected to refresh them into.env. Requests also send a desktop-browserUser-Agent.Deploy:
deploy/brightspace-mcp.serviceruns thebrightspacemcpconsole script under systemd withuv run --frozen --no-sync(never touches the lockfile at boot) and a strict sandbox (ProtectSystem=strict,ProtectHome=read-only, restricted address families, private tmp).
Caveat
My inbound side has self-generated token auth at the moment and the outbound side talks to the D2L api with a scraped session cookie, not an OAuth 2.0 app.
"Unfortunately, we have not yet provided students with OAuth 2.0 access to Brightspace for personal use."
Because of this, it is single-user, tied to a single account, breaks on session expiry, and is likely against API terms. It is a personal tool, not a multi-user service. A future production version would also be able to register a D2L app and use the OAuth flow for the Brightspace call.
Cookie sync (browser extension)
The "separate process expected to refresh them into .env" mentioned above:
extension/ is a Manifest V3 browser extension that watches the two session
cookies on purdue.brightspace.com and, whenever either changes (i.e. you log
in again from wherever you actually browse Brightspace), pushes them to a
small receiver running on this host, which rewrites .env and restarts
brightspace-mcp.service so the new session takes effect immediately — no
more manually copying cookies out of DevTools.
purdue.brightspace.com (your browser, any device)
│ chrome.cookies.onChanged
▼
extension/background.js
│ HTTPS POST /cookies + Authorization: Bearer <COOKIE_SYNC_TOKEN>
▼
nginx (cookie-sync.xennick.com, TLS termination)
│ HTTP
▼
cookie_sync_server.py (127.0.0.1:8010) ← this repo
│ upserts .env, then `sudo -n systemctl restart brightspace-mcp.service`
▼
brightspace-mcp.service restarts with the fresh cookiesServer-side setup:
python mint_token.py --var COOKIE_SYNC_TOKEN --force # mints and writes .env
sudo install -m 440 deploy/sudoers-brightspace-cookie-sync /etc/sudoers.d/brightspace-cookie-sync
sudo visudo -cf /etc/sudoers.d/brightspace-cookie-sync
sudo cp deploy/brightspace-cookie-sync.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now brightspace-cookie-sync
# add the cert if xennick.com isn't already a wildcard/SAN cert (see the
# comment at the top of the conf file), then:
sudo cp deploy/cookie-sync.xennick.com.conf /etc/nginx/sites-available/
sudo ln -s /etc/nginx/sites-available/cookie-sync.xennick.com.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxThe sudoers rule scopes nick to exactly one command
(systemctl restart brightspace-mcp.service) — that's the actual privilege
boundary, which is why brightspace-cookie-sync.service can otherwise stay
locked down (ProtectSystem=strict, ReadWritePaths limited to .env).
Extension setup (Chrome/Edge/Brave — loaded unpacked since this is single-user and never goes near the Web Store):
chrome://extensions→ enable Developer mode → Load unpacked → selectextension/.Open the extension's options page, paste the
COOKIE_SYNC_TOKENfrom above and confirm the endpoint (https://cookie-sync.xennick.com/cookiesby default), Save, then Test connection.Log into
purdue.brightspace.comnormally. The extension pushes the new cookies automatically; check its popup for the last sync status, or hit Sync now there if you don't want to wait for the next natural cookie change.
Optional tools
getLink — launches the course's Kaltura LTI in a headless Playwright
browser, grabs the video's index.m3u8, and runs it through openai-whisper
for a transcript. Its dependencies live in the transcription extra
(playwright + openai-whisper, which pulls PyTorch), kept out of the base
install because they're large and the transcription itself is slow on a small
host.
The base server imports fine without them;
server.pychecksimportlib.util.find_specat startup (_HAS_TRANSCRIPTION) and only callsmcp.tool()(getLink)when both are present, so a lean install advertises exactly the tools it can run.Called without the extra (e.g. registered by hand),
getLinkreturns{"error": "transcription extra not installed", "hint": ...}.
Enable it with:
uv sync --extra transcription
playwright install chromiumgetLTILink — resolves /d2l/common/dialogs/quickLink/... redirects (a thin
wrapper over a raw authenticated GET). Checked in with its @mcp.tool() line
commented out; un-comment to register it (no extra dependencies).
Setup
Requires uv. Python ≥ 3.14 is fetched by uv
automatically — you don't need it installed already.
Guided (recommended)
./setup.shIt's interactive and safe to re-run. It will:
install
uvif it's missing (offers to run the official installer),uv sync(and, if you say yes, the heavytranscriptionextra),prompt for your two Brightspace session cookies,
mint the inbound bearer token via
mint_token.py,write
.env, andprint the line to paste into your MCP client:
Copy this whole token for input: "Bearer <token>"
Then run the server:
uv run brightspacemcp # or: python -m brightspacemcpIt listens on http://127.0.0.1:8008. Point an MCP client at that (directly, or
through a TLS proxy as above).
Manual
uv syncThe base uv sync is lean — mcp, httpx2, python-dotenv. See
Optional tools for the transcription extra.
Create .env with a current browser session's cookies:
d2lSessionVal=...
d2lSecureSessionVal=...
MCP_INBOUND_TOKEN=... # bearer token MCP clients must send
MCP_PUBLIC_HOST=... # optional; public hostname nginx serves (default mcp.xennick.com)Copy the d2l* cookies from your browser's dev tools while logged into
purdue.brightspace.com (DevTools → Application / Storage → Cookies — the
entries named d2lSessionVal and d2lSecureSessionVal). For the token, run
uv run python mint_token.pywhich generates one, upserts MCP_INBOUND_TOKEN into .env (leaving your other
lines alone), and prints the Bearer … string. --show reprints the current
one; --force replaces it.
Then run the server as above.
Refreshing expired cookies
When tools start returning 401/403, your d2l* cookies have expired. If the
cookie-sync extension is set up, just log
back into purdue.brightspace.com and it refreshes .env and restarts the
service for you. Otherwise, re-run ./setup.sh (press enter to keep the token
and re-paste the two cookies), or edit the two d2l* lines in .env by hand.
Deploy as a service
cp deploy/brightspace-mcp.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now brightspace-mcp.serviceAdjust User, WorkingDirectory, and the uv path in the unit first. After
changing dependencies, run uv sync by hand — the unit deliberately runs
--frozen --no-sync.
Layout
src/brightspacemcp/
__init__.py exports main()
__main__.py python -m brightspacemcp
server.py MCPServer, all @mcp.tool() definitions, request helpers
(JSON / file download), feed-merge logic, RequireToken
auth.py Brightspace session cookies from .env (outbound)
setup.sh interactive first-run setup (deps + .env + token)
mint_token.py mint a bearer token (MCP_INBOUND_TOKEN or --var NAME) into .env
cookie_sync_server.py receives cookie pushes from extension/, rewrites .env,
restarts brightspace-mcp.service
extension/ Manifest V3 browser extension: watches d2l* cookies, POSTs
changes to cookie_sync_server.py
deploy/
brightspace-mcp.service
brightspace-cookie-sync.service
cookie-sync.xennick.com.conf
sudoers-brightspace-cookie-sync
pyproject.toml uv_build, src layout, `brightspacemcp` console scriptThis server cannot be deployed
Maintenance
Related MCP Connectors
Connect your Moodle to AI assistants: courses, content, grading and reports from the chat.
Provides cloud browser automation capabilities using Stagehand and Browserbase, enabling LLMs to i…
Create, edit, translate, and export SCORM eLearning modules from a connected AI assistant.
Manage your Canvas coursework with quick access to courses, assignments, and grades. Track upcomin…
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to interact with Canvas LMS through 25 comprehensive tools. Supports course management, assignments, grades, messaging, calendar events, and file access through natural language.111 npm14MIT
- FlicenseAqualityNot gradedmaintenanceEnables interaction with Canvas LMS courses and assignments directly from your LLM, allowing you to retrieve, search, and summarize course information, check due dates, and access assignment details without leaving your AI assistant.4111 npm-
- AlicenseAqualityDmaintenanceEnables AI assistants to interact with D2L Brightspace LMS, providing access to assignments, grades, course content, calendar events, and announcements through automated SSO authentication.129 npm12MIT
- FlicenseBqualityDmaintenanceEnables interaction with Canvas LMS to access courses, modules, files, pages, assignments, submissions, announcements, upcoming deadlines, and syllabus.15-