Skip to content

Security & scopes

Two accepted credentials:

  • X-MCP-Key — an MCPKey bearer minted from Django admin.
  • The platform’s own JWT / session — an Open edX JWT already encodes is_staff / is_superuser, so the same endpoints work with a raw platform token too.

Whichever credential authenticates, the rule is identical and re-checked live on every request: the acting user must be is_staff or is_superuser right now. Demote them and every key they hold stops working on the next call. A key never caches privilege; scopes only ever narrow it.

Tick scopes per key for least privilege. A superuser bypasses narrowing.

ScopeGates
readwhoami, analytics, listings, roles, grades, cert/report status
write:enrollmentenroll, unenroll, bulk_enroll
write:userscreate_user, reset_student_attempts
write:rolesset_role (course roles), instructor_access
grant:adminset_role for global_staff / superuser (escalation)
write:certificatesgenerate / regenerate certificates
write:reportssubmit async reports (grade export, …)
write:coursesblock CRUD, create_block_tree, update_course_settings
destructiveadditive — deactivate_user, request_retirement, invalidate_certificate, delete block

destructive is additive: a destructive tool needs its domain scope and destructive, so a key can never delete without also holding the relevant write scope. Granting platform admin is isolated behind grant:admin, so an ops key can manage course staff without ever being able to mint a superuser.

Every state-changing tool passes through four rails:

  1. Live authority re-check — the gate above, per request.
  2. Rate limit — per-(key, tool) fixed window; turns a runaway agent into ~N calls instead of thousands. Mass tools (bulk enroll, retire) get the tightest budgets.
  3. Dry-run + confirm token — high-impact / destructive tools first return a preview plus a single-use token bound to a fingerprint of the exact payload. Re-send with the token to apply; changing any argument invalidates it.
  4. Append-only audit — intent is recorded before the write, and the write is refused if that record can’t be persisted.