lamaku-mcp
v0.1.1
Published
MCP server for Lamakū, the University of Hawaiʻi's D2L Brightspace LMS — read course data and author announcements, content, assignments, grade items and discussions, with a FERPA guard on student identities
Maintainers
Readme
lamaku-mcp
An MCP server for Lamakū, the University of Hawaiʻi's D2L Brightspace LMS.
It reads your course data, and unlike the other Brightspace MCP servers I know of, it also writes: announcements, content modules, authored HTML pages, uploaded media, links, assignments, grade items, discussion forums and topics, and whole course packages.
Student identities are pseudonymised by default. See FERPA guard.
Overview states what it does and what it does not do, then installs in four commands. The manual covers the safety model, building a course end to end, the page templates, and all 54 tools.
What this is for
Building and maintaining a course from an agent, without clicking through Brightspace.
The three D2L MCP servers I could find are read-only. They check grades and due dates, which is useful, and it is a different job from this one. If you want to ask what is due on Thursday, those are simpler. If you want to construct a course and put it in Lamakū, this is the one that can.
The trade is that writes land in a real course, so most of the design here is about not doing that carelessly. See Safety model.
Status
Every write route below was verified against a real Lamakū sandbox course by creating an object and deleting it again. None of it is inferred from the D2L documentation alone, which turned out to be wrong or incomplete on several payload shapes.
| Capability | Status |
|---|---|
| Announcements, create and delete | verified |
| Content modules, create, nest, delete | verified |
| Content links (URL topics) | verified |
| Assignment folders and categories | verified |
| Grade items and categories | verified |
| Discussion forums and topics | verified |
| Discussion delete, forums and topics | verified, with the blast radius named in the preview |
| Checklists, nested categories and items, append, delete | verified |
| Quizzes, create and delete | verified, but see quiz questions below |
| Content pages, authored HTML, three templates | verified |
| Media upload, video, audio, images, PDF, captions | verified up to 36 MB |
| Content visibility, release to students, reorder, rename | verified |
| Course package import, .imscc or Brightspace .zip | verified |
| Quiz questions, by import | verified: 17 questions with answer keys and feedback |
| Reading everything above, plus surveys, calendar, classlist | verified |
| Quiz questions, directly | Not possible. Brightspace exposes GET but no create route, so create_quiz makes an empty shell. Import a cartridge instead. |
| Rubric authoring | Blocked. The route needs le 1.97+ and Lamakū serves 1.96, one version short. A Brightspace .zip import carries rubrics today, which routes around it. |
| Rubric assessment (scoring against an existing rubric) | Available but unproven. /assessment needs only le 1.93+ and answers on 1.96. Untested because the sandbox has no rubrics to score against. |
| Grade value writing | Untested. The sandbox has no student enrolments, and trying it in a live section would alter a real student's record. |
| Grade item listing | Not possible. get_grades returns the caller's own grades, so an item can be created and deleted by id but never enumerated. |
| Course creation | Org-level admin, not available to an instructor account. Ask UH ITS. |
Reading a 404 against a 400
Brightspace separates these in a way worth relying on when probing. A 404 means the route does
not exist at that API version. A 400 means it exists and rejected your input. That is how the
rubric split above was settled: /rubrics/ returns 404 on 1.96 while /assessment returns 400,
matching the documented 1.97 and 1.93 minimums exactly.
One caveat on the same theme. A 400 does not prove you have permission. Several routes validate
the body before checking the role, so probing with a malformed payload over-reports your access.
Only a real create proves anything.
Roles matter more than you would expect
The same person holds different roles in different courses, and the API enforces them
differently. On one account: Instructor on sandboxes, Designer on one section,
Teaching Assistant on another, Participant elsewhere, and Instructor-Content Copy Only on a
template. That last one passes on announcements and is refused by the assignment routes.
Every authoring tool runs a role preflight before spending the call, so you get a straight explanation instead of a bare 403.
Setup
npx -y lamaku-mcp login # opens a browser; complete UH login and Duo yourself
claude mcp add lamaku -- npx -y lamaku-mcpGemini CLI reads the same JSON shape from ~/.gemini/settings.json; Codex CLI takes
codex mcp add lamaku -- npx -y lamaku-mcp or a [mcp_servers.lamaku] block in
~/.codex/config.toml with the same command and args.
Or for any MCP client that speaks stdio:
{
"mcpServers": {
"lamaku": {
"command": "npx",
"args": ["-y", "lamaku-mcp"],
"env": { "LAMAKU_HOST": "lamaku.hawaii.edu" }
}
}
}From source
For working on the server itself:
git clone https://github.com/JesseTho/lamaku-mcp
cd lamaku-mcp && pnpm install && pnpm build
claude mcp add lamaku-dev -- node "$PWD/dist/index.js"The committed lockfile is pnpm's, and CI installs with --frozen-lockfile. npm works fine
locally if you prefer it, but do not commit the package-lock.json it generates.
The login opens a real browser window because UH requires Duo. Sessions last about a day of
idleness and expire hard after a few days, at which point you run login again.
Configuration
| Variable | Default | Purpose |
|---|---|---|
| LAMAKU_HOST | lamaku.hawaii.edu | Brightspace hostname. Any D2L instance should work, though only Lamakū is tested. |
| BRIGHTSPACE_HOST | unset | Neutral alias for the same thing. LAMAKU_HOST wins if both are set. |
| LAMAKU_FERPA | strict | off disables student pseudonymisation. |
| LAMAKU_AUTH | session | oauth uses a registered client instead of a captured browser session. Scaffolded, not proven. |
| LAMAKU_BROWSER | auto | Force chrome, msedge, or chromium at login. |
| LAMAKU_DOWNLOAD_DIR | under app data | Where downloaded files land. |
| LAMAKU_LE_VERSION, LAMAKU_LP_VERSION | latest | Pin an API version. |
FERPA guard
Student names, usernames, emails and institutional IDs are protected education records. This server hands data to a language model, and from there into a vendor's logs, so by default it does not emit them.
Students become a stable handle like student:4f2a91. The handle is an HMAC under a salt
generated on your machine and never transmitted, so it is not reversible and not comparable
across installs. It stays stable across calls, which means an assistant can still reason about
"the same student who missed lab 3" without knowing who that is.
The raw userId is dropped as well. It is a direct key back into Brightspace and into anything
sharing the same institutional ID, so pseudonymising the name alone would not be enough.
Course staff are not redacted. A co-instructor's name is not a protected record, and hiding it
makes a roster unreadable for no gain. Passing revealStudents: true on a call returns real
names, for when you have deliberately asked.
This is a disclosure control rather than an access control. You can read your roster in Brightspace whenever you like. The point is keeping it out of prompts and model retention unless you meant to put it there.
pnpm test # includes the guard's unit tests
node scripts/verify-privacy.mjs # asserts nothing leaks end to endSECURITY.md covers what the guard does and does not protect, and how session storage actually works.
Safety model
Writes are irreversible and land in a real course, so every authoring tool is gated twice.
First, a role preflight refuses before the call if your role in that course cannot perform the action. Second, the first call returns only a preview of what would be sent. Nothing reaches Brightspace until you call again with the token that preview carries. Tokens are single use, expire in five minutes, and are scoped to the action they were minted for, so a token approved for one preview cannot authorise a different operation.
New objects are created hidden from students. release_course_content is the deliberate step
that publishes them, and it previews exactly what becomes visible first.
Deletes are permanent and cannot be undone through this API. The previews say so, and name what goes with the object. A forum preview lists every topic that will go with it.
LAMAKU_SANDBOX=<courseId> node scripts/verify-writes.mjsThat script creates and deletes real objects, so point it only at a sandbox. It refuses to run
without an explicit course id. scripts/README.md says which of the others
need a live session and which are safe anywhere.
Tests
pnpm test # unit tests, no Brightspace needed
pnpm typecheckThe unit tests cover the places where a bug is silent rather than loud: multipart framing, the two rich-text shapes, the confirmation gate's single use and action scoping, and the FERPA guard. They need no credentials and run in CI on Node 20 and 22.
The tests are plain .mjs against the compiled output in dist, so building is part of
testing and there are no experimental flags to keep working. That is also what keeps the Node 20
floor honest: type stripping needs 22.6, and the package claims 20.
Writing them found a real bug, which is the argument for having them. scrubNames anchored every
name part with a trailing \b, so a part ending in punctuation never matched. Brightspace display
names are frequently "Last, First", which meant Smith, went through unscrubbed. It had looked
fine for months.
The verify-*.mjs scripts are integration checks and stay manual. They need a live session, and
verify-writes.mjs creates real objects.
Tools
Session auth_status, whoami, check_capabilities
Reading list_courses, list_assignments, get_assignment, list_my_submissions,
download_submission_file, get_grades, get_final_grade, get_upcoming_deadlines,
list_modules, get_module, get_topic, download_topic_file, get_announcements,
list_forums, list_topics, read_posts, list_quizzes, list_checklists
Authoring create_announcement, delete_announcement, create_content_module,
create_content_link, delete_content_module, create_assignment,
create_assignment_category, delete_assignment, create_grade_item, create_grade_category,
delete_grade_item, create_discussion_forum, create_discussion_topic, create_quiz,
delete_quiz, create_checklist, add_checklist_item, delete_checklist,
create_content_page, update_content_page, create_content_file, update_content_module,
update_content_topic, set_module_description, release_course_content,
delete_content_topic, delete_discussion_forum, delete_discussion_topic,
import_course_package, get_import_status
Import reads a file from the machine the server runs on, so an assistant can write a cartridge and
import it in the same session. That is how quiz questions are created: Brightspace has no route for
them, and the QTI in the cartridge becomes the questions. Rubrics need the Brightspace .zip format,
which is best obtained by exporting from a course that already has one.
Student side submit_assignment, create_discussion_post, reply_to_post
pnpm tools # print the live list with signaturesAuthoring a course end to end
Order matters here, mostly because media paths are not guessable until the media exists.
create_content_module, one per module.create_content_fileto upload video, audio and images. Each call returns the enforced content path and anembedAssnippet.create_content_page, pasting those snippets where the media belongs. Doing this before the upload means rewriting every page once you know the paths.create_content_linkfor external sources. Thencreate_discussion_forum,create_discussion_topicandcreate_assignmentfor the parts a Common Cartridge cannot carry at all.set_module_descriptionto give each module a cover image, once its header image is uploaded. Brightspace renders an image in a module description as that module's cover.import_course_packagefor anything the API cannot author. Quiz questions are the clearest case. Pollget_import_statusuntil it reportsCOMPLETED.release_course_contentlast, since everything above was created hidden on purpose.
Page templates
create_content_page and update_content_page take a template:
| Template | What it does |
|---|---|
| uh (default) | Links the UH shared HTML Template Library: banner, content column, seal footer. Correct on a UH instance and nowhere else. |
| jabsom | JABSOM Design System with its tokens inlined. Mānoa Green headings in Inter, Source Serif 4 body. Self-contained, so it renders correctly off-instance too. |
| plain | A bare document with no institutional styling. |
docs/course-style.md covers the UH template and
docs/jabsom-style.md the JABSOM one, including the available components
and the accessibility obligations that come with them.
What an import can carry
A Common Cartridge brings pages, weblinks, files, QTI quizzes and question banks, discussion
topics and LTI links. A Brightspace .zip export brings all of that plus D2L-native objects:
rubrics, release conditions, grade schemes. That is the practical way to move a rubric between
courses while the rubric API waits on le 1.97. Groups, sections, attendance and awards cannot be
expressed in either format.
Import adds, it does not replace. Importing the same package twice produces two copies of everything, so import into an empty course or check for overlap first.
Notes for anyone extending this
Rich text is not one shape, and which one an endpoint accepts is not derivable from the API version, the product code, or the parent object:
| Shape | Used by |
|---|---|
| {Content, Type} | grades, content modules, dropbox folders, discussion topics |
| {Text, Html} | discussion forums, announcements |
A forum and a topic inside that forum disagree. Sending the wrong one yields a bare
400 Invalid Parameters naming no field. The shapes are pinned per endpoint in
src/api/richtext.ts.
Other traps, all of which cost an afternoon at some point:
- Announcements need
multipart/mixed, not JSON and notform-data, andStartDateis mandatory even for an unpublished draft. GradeSchemeIdmust be0for "course default". Sendingnullis rejected.- Discussion
RatingTypeis a string enum. Sending0is rejected. - Quiz creation rejects partial bodies. Every documented field has to be present, and a
nearly-complete payload,
{},[]and malformed JSON all return the same opaqueProvided JSON is invalidwith no field named. An incomplete body is therefore indistinguishable from an unsupported endpoint. The full field set is insrc/tools/instructor/quizzes.ts. - Checklists need
SortOrderof at least 1 on both categories and items, and an item'sCategoryIdis required and non-nullable. A checklist is not usable until it has a category. - Collection shapes are inconsistent within a single feature.
GET /checklists/returns a bare array while its own/categories/and/items/sub-routes wrap the same data in{Objects, Next}. Normalise both. PUTreplaces rather than patches. Read the object first and send it back with your changes applied, or the fields you omitted are silently blanked.
Acknowledgements
Derived from mycourses-mcp by Sahil Dayal (MIT), which contributed the browser-session auth, the confirmation gate, and the client and multipart layers. The instructor authoring tools, the FERPA guard, the role preflight and the Lamakū targeting are new here.
Every tool disappeared mid-session
The server process died. A stdio server has nowhere to report that, and
claude mcp list will still say it is connected, because checking spawns a
fresh process rather than inspecting the one your session is talking to.
Restart the client to get the tools back. The server now logs the cause to stderr and survives an unhandled rejection rather than exiting on it, so a repeat should leave a trail.
Not endorsed by the University of Hawaiʻi or D2L. Check UH's acceptable-use policy before pointing this at your account.
Credits
MIT licensed. Derived from mycourses-mcp by
Sahil Dayal. Site icons are from Lucide, ISC licensed; the notice also
rides with the vendored path data in docs/.
