Skip to content

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

  • domain cannot import application, infrastructure, or any context other than itself.
  • application cannot import infrastructure (only domain interfaces).
  • Cross-context imports go through domain types only — no import x from '@/contexts/foo/infrastructure/...'.

Bounded contexts

ContextAggregate rootsResponsibilities
authenticationSession, UserIdentityMulti-strategy auth, MFA solving, session caching
coursesCourseEnrolment list, course metadata
assignmentsAssignment, Submission, FeedbackDropbox folders, submissions, grades feedback
gradesGradeFinal grades per course
contentModule, TopicCourse content tree, file extraction
communicationsAnnouncement, DiscussionNews feed, forum threads
calendarCalendarEventCourse calendar
groupsGroupGroup enrollments and member rosters
notificationsNotificationUser activity feed
quizzesQuiz, QuizAttemptQuiz metadata and attempt history
http-apiD2lApiClientShared HTTP client, resilience, XSRF, auth wiring

Composition root

src/composition-root.ts wires everything:

  1. Load config from ~/.brightspace-mcp/config.yaml (or --config/BRIGHTSPACE_CONFIG).
  2. Resolve credentials from env: / keychain: / file: references.
  3. Build the D2lHttpClient with the right auth strategy (or chain).
  4. Instantiate one repository per context (D2L impl + cache decorator).
  5. Create the WritesGate from config + CLI flag.
  6. Build OutputContext from config.output (tz, locale, format).
  7. Pass everything as ToolDeps to registerAllTools(server, deps), registerAllResources(server, deps), and registerAllPrompts(server, deps).

Adding a new MCP tool

Recipe (the submit_assignment writes-gated example is good to copy):

  1. Domain — add the type/method to src/contexts/<ctx>/domain/<X>Repository.ts if needed.
  2. Application — add a use case src/contexts/<ctx>/application/<verb><Noun>.ts. Keep it small: input → repository call → output.
  3. Infrastructure — implement the new repository method in src/contexts/<ctx>/infrastructure/D2l<X>Repository.ts.
  4. Schema — add the Zod input schema to src/mcp/schemas.ts.
  5. Tool handler — add src/mcp/tools/<verb>-<noun>.tool.ts exporting handle<X> and a <X>Deps interface.
  6. Registrysrc/mcp/registry.ts → register inside registerAllTools. If it's a write, gate behind deps.writesGate.allowsWrites.
  7. 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

ComponentWhere
Config loading + schema validationsrc/shared-kernel/config/
Credential refs (env:, keychain:, file:)src/shared-kernel/credentials/
Audit logger (NDJSON to disk)src/shared-kernel/audit/
Idempotency storesrc/shared-kernel/idempotency/
Writes gatesrc/shared-kernel/writes/WritesGate.ts
ZIP/DOCX/XLSX text extractionsrc/shared-kernel/zip/extractZipEntry.ts
Path expansion (~/, %VAR%, absolute)src/mcp/tools/get-topic-file.tool.ts (helper)
Locale, timezone, markdown formattingsrc/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 server
  • setup — interactive YAML wizard (displays in system language)
  • init — non-interactive config writer (CI/scripts, no TTY required); supports --preset microsoft
  • ui — local web dashboard at http://localhost:9876 (Hono HTTP server, 10 pages, dark/light mode)
  • auth — manual re-auth, useful when sessions expire
  • record-auth — opens a real browser, you log in manually, cookies captured into session_cookie strategy
  • doctor — end-to-end smoke test: config → auth → API → list_my_courses
  • upgrade — upgrade brightspace-mcp to the latest version with a version notification on next serve
  • profile list / use <name> — list and switch profiles
  • config show / validate / set — inspect / edit YAML
  • cache clear / status — cache management

Test architecture

  • Unit tests mirror src/ in tests/.
  • Integration tests live in tests/integration/ and use real Vitest + nock HTTP 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

  1. npm version patch|minor|major (or edit package.json + CHANGELOG.md manually).
  2. Push tag — CI runs check + build:clean.
  3. 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

Released under the MIT License.