Architecture overview
brightspace-mcp follows Domain-Driven Design with bounded contexts. Layering is enforced at build time by dependency-cruiser.
Layering
Rules (enforced by npm run check:deps):
domaincannot importapplication,infrastructure, or any context other than itself.applicationcannot importinfrastructure(only domain interfaces).- Cross-context imports go through domain types only — no
import x from '@/contexts/foo/infrastructure/...'.
Bounded contexts
| Context | Aggregate roots | Responsibilities |
|---|---|---|
authentication | Session, UserIdentity | Multi-strategy auth, MFA solving, session caching |
courses | Course | Enrolment list, course metadata |
assignments | Assignment, Submission, Feedback | Dropbox folders, submissions, grades feedback |
grades | Grade | Final grades per course |
content | Module, Topic | Course content tree, file extraction |
communications | Announcement, Discussion | News feed, forum threads |
calendar | CalendarEvent | Course calendar |
groups | Group | Group enrollments and member rosters |
notifications | Notification | User activity feed |
quizzes | Quiz, QuizAttempt | Quiz metadata and attempt history |
http-api | D2lApiClient | Shared HTTP client, resilience, XSRF, auth wiring |
Composition root
src/composition-root.ts wires everything:
- Load config from
~/.brightspace-mcp/config.yaml(or--config/BRIGHTSPACE_CONFIG). - Resolve credentials from
env:/keychain:/file:references. - Build the
D2lHttpClientwith the right auth strategy (or chain). - Instantiate one repository per context (D2L impl + cache decorator).
- Create the
WritesGatefrom config + CLI flag. - Build
OutputContextfromconfig.output(tz, locale, format). - Pass everything as
ToolDepstoregisterAllTools(server, deps),registerAllResources(server, deps), andregisterAllPrompts(server, deps).
Adding a new MCP tool
Recipe (the submit_assignment writes-gated example is good to copy):
- Domain — add the type/method to
src/contexts/<ctx>/domain/<X>Repository.tsif needed. - Application — add a use case
src/contexts/<ctx>/application/<verb><Noun>.ts. Keep it small: input → repository call → output. - Infrastructure — implement the new repository method in
src/contexts/<ctx>/infrastructure/D2l<X>Repository.ts. - Schema — add the Zod input schema to
src/mcp/schemas.ts. - Tool handler — add
src/mcp/tools/<verb>-<noun>.tool.tsexportinghandle<X>and a<X>Depsinterface. - Registry —
src/mcp/registry.ts→ register insideregisterAllTools. If it's a write, gate behinddeps.writesGate.allowsWrites. - Tests — mirror the file in
tests/with both unit and integration coverage.
Add a new Resource: Create src/mcp/resources/<name>.resource.ts with registerXxxResource(server, deps). Register it in src/mcp/resources/registry.ts.
Add a new Prompt: Create src/mcp/prompts/<name>.prompt.ts with registerXxxPrompt(server, deps). Register it in src/mcp/prompts/registry.ts. Add prompt text to all 4 i18n catalogs under prompts.<name>.*.
Run npm run check before pushing. Coverage threshold is 85% statements.
Cross-cutting components
| Component | Where |
|---|---|
| Config loading + schema validation | src/shared-kernel/config/ |
Credential refs (env:, keychain:, file:) | src/shared-kernel/credentials/ |
| Audit logger (NDJSON to disk) | src/shared-kernel/audit/ |
| Idempotency store | src/shared-kernel/idempotency/ |
| Writes gate | src/shared-kernel/writes/WritesGate.ts |
| ZIP/DOCX/XLSX text extraction | src/shared-kernel/zip/extractZipEntry.ts |
Path expansion (~/, %VAR%, absolute) | src/mcp/tools/get-topic-file.tool.ts (helper) |
| Locale, timezone, markdown formatting | src/shared-kernel/output/ |
| Web dashboard (Hono HTTP server + Alpine.js SPA) | src/cli/commands/ui.ts + src/ui/ |
CLI commands
src/cli/commands/:
serve— start the MCP stdio serversetup— interactive YAML wizard (displays in system language)init— non-interactive config writer (CI/scripts, no TTY required); supports--preset microsoftui— local web dashboard athttp://localhost:9876(Hono HTTP server, 10 pages, dark/light mode)auth— manual re-auth, useful when sessions expirerecord-auth— opens a real browser, you log in manually, cookies captured intosession_cookiestrategydoctor— end-to-end smoke test: config → auth → API →list_my_coursesupgrade— upgrade brightspace-mcp to the latest version with a version notification on next serveprofile list / use <name>— list and switch profilesconfig show / validate / set— inspect / edit YAMLcache clear / status— cache management
Test architecture
- Unit tests mirror
src/intests/. - Integration tests live in
tests/integration/and use real Vitest +nockHTTP mocking. - End-to-end in
tests/e2e/boot a real Node subprocess and speak MCP over stdio.
vitest.config.ts enforces 85% statement coverage.
Release flow
npm version patch|minor|major(or editpackage.json+CHANGELOG.mdmanually).- Push tag — CI runs
check+build:clean. - CI publishes to npm and builds the Docker image.
CI files:
.github/workflows/ci.yml— lint + test + depcruise + coverage.github/workflows/release.yml— npm publish on tag.github/workflows/docker.yml— image build & push