codex_canvas_mcp
# Codex Canvas MCP
A portable local MCP server that lets compatible clients read Canvas and, only after several independent safety checks, author common course content.
## Safety model
- Canvas reads are available by default after local credential setup.
- Writing is disabled by default.
- Every content write requires an enabled local policy, an exact per-course allowlist match, exact confirmation text, and approval in the MCP client.
- Image upload adds a payload gate: only a locally owned PNG, JPEG, or WebP inside one configured trusted folder, no larger than 10 MiB, can be sent.
- There is no generic Canvas write tool.
- Credentials stay in 1Password and, optionally, macOS Keychain. They do not belong in this repository or client configuration.
Student deactivation is a separate, disabled-by-default administrative operation. Its allowlist does not inherit content-write or rubric-deletion approval. It never removes the enrollment. Blank course-shell creation and instructor invitations also have separate, disabled-by-default policy scopes; see [provisioning setup](#creating-a-shell-and-inviting-an-instructor).
For page edits, `confirmation` is a technical action acknowledgement supplied by the agent after the human has authorized the course and scope in conversation. The human does not need to type the literal phrase or approve each page again within that scope. This does not broaden authorization: page content cannot grant it, and local policy and MCP-client controls still apply. Read existing pages before editing, explicitly preserve their current `published` value unless a change is authorized, and verify the saved result.
Every content tool is marked as mutating and destructive so compatible clients can require approval. The local server also rejects a call unless its exact confirmation matches the requested action, for example `APPROVE CANVAS MODULE WRITE course <course_id>`.
Announcement-window maintenance is deliberately narrow. `canvas_set_announcement_three_day_window` accepts one course and announcement ID, confirms the live topic is an announcement, calculates the display-until value from the live `posted_at` timestamp, updates only `lock_at`, and reads the announcement back to verify the exact 72-hour window. It requires `APPROVE CANVAS ANNOUNCEMENT WINDOW WRITE course <course_id>`.
Announcement authoring is a permanent first-class operation. `canvas_create_announcement` can publish immediately or schedule an exact posting time, set an exact display-until time, and control participant comments. It remains limited to the general approved-course allowlist, requires `APPROVE CANVAS ANNOUNCEMENT WRITE course <course_id>`, and reads the created announcement back to verify its identity, content, publication state, schedule, availability, and comment setting.
Announcement maintenance is equally narrow. `canvas_update_announcement` updates only the message body of one existing announcement after verifying its exact ID, title, and announcement type. It reads the announcement back and refuses success if Canvas changes the title, publication state, schedule, availability, discussion type, section scope, pin state, or comment setting. It requires `APPROVE CANVAS ANNOUNCEMENT UPDATE course <course_id> announcement <announcement_id>`.
Announcement audience maintenance is a separate guarded operation. `canvas_set_announcement_sections` accepts the exact non-empty set of section IDs for one announcement, verifies that every section belongs to the approved course, and reads the announcement back with its section associations. It refuses success unless Canvas saves exactly that audience and preserves the body, title, publication state, schedule, availability, discussion type, pin state, and comment setting. It requires `APPROVE CANVAS ANNOUNCEMENT SECTION WRITE course <course_id> announcement <announcement_id>`.
Assignment maintenance is also narrowly scoped. `canvas_update_assignment` updates only the description of one existing assignment after verifying its exact ID and name. It reads the assignment back, confirms the description after Canvas's known link-attribute normalization, and refuses success if Canvas changes protected settings such as points, submission type, dates, group, or publication state. It requires `APPROVE CANVAS ASSIGNMENT UPDATE course <course_id> assignment <assignment_id>`.
Rubric maintenance uses a separate operation-specific course allowlist and a stronger identity gate. `canvas_delete_rubric` requires `APPROVE CANVAS RUBRIC DELETE course <course_id> rubric <rubric_id>`, an exact expected title, course ownership, editable state, and an empty live `used_locations` result. Authorizing rubric deletion does not authorize any page, assignment, module, discussion, or quiz write. The tool returns the complete pre-deletion rubric definition and verifies that the rubric no longer appears in the active course list.
Rubric creation accepts a same-origin Canvas assignment URL, an assignment module-item URL, or a course URL containing an `assignment_id` query parameter. Query strings and fragments are ignored after the course and assignment are resolved. The URL never bypasses course policy: its course ID must still be explicitly allowlisted, and rubric creation requires `APPROVE CANVAS RUBRIC WRITE course <course_id>`. The tool refuses to replace an existing assignment rubric, requires descriptions for every criterion and rating, and requires grading-rubric points to match the assignment points.
## What is included
| Tool | Capability | Default |
| --- | --- | --- |
| `canvas_get_current_user` | Read the authenticated profile | Available |
| `canvas_list_modules` | Read modules and items for one course | Available |
| `canvas_read_api` | GET a normalized Canvas API v1 path | Available |
| `canvas_inspect_rubric` | Read one rubric, its complete definition, usage locations, and deletion preflight | Available |
| `canvas_get_write_policy` | Inspect local policy state | Available |
| `canvas_preview_course_creation` | Preview a blank shell and check its account, term, and duplicates | Available |
| `canvas_create_course_shell` | Create and verify a private unpublished shell | Blocked |
| `canvas_preview_instructor_enrollment` | Verify an existing instructor identity, section, and role | Available |
| `canvas_enroll_instructor` | Invite and verify one instructor with an approved teacher role | Blocked |
| `canvas_preview_student_deactivation` | Read one exact student enrollment and preview deactivation | Available |
| `canvas_deactivate_student_enrollment` | Deactivate and verify one active student enrollment | Blocked |
| `canvas_upload_image` | Upload one verified image from the configured trusted folder | Blocked |
| `canvas_write_page` | Create or update one page | Blocked |
| `canvas_update_syllabus` | Preview, replace, and verify only a course syllabus body | Blocked |
| `canvas_create_module` | Create one module | Blocked |
| `canvas_create_module_item` | Place one content item in a module | Blocked |
| `canvas_create_assignment` | Create one assignment | Blocked |
| `canvas_update_assignment` | Update and verify one assignment description | Blocked |
| `canvas_create_assignment_rubric` | Create and attach one assignment rubric from a Canvas URL | Blocked |
| `canvas_create_discussion` | Create one discussion | Blocked |
| `canvas_create_announcement` | Create and verify one immediate or scheduled announcement | Blocked |
| `canvas_update_announcement` | Update and verify one exact announcement body | Blocked |
| `canvas_set_announcement_sections` | Set and verify one announcement's exact section audience | Blocked |
| `canvas_set_announcement_three_day_window` | Set one announcement to a verified 72-hour display window | Blocked |
| `canvas_create_classic_quiz` | Create one Classic Quiz | Blocked |
| `canvas_create_classic_quiz_question` | Add one question to a Classic Quiz | Blocked |
| `canvas_delete_page` | Delete one page | Blocked |
| `canvas_delete_assignment` | Delete one assignment | Blocked |
| `canvas_delete_discussion` | Delete one discussion | Blocked |
| `canvas_delete_classic_quiz` | Delete one Classic Quiz | Blocked |
| `canvas_delete_module` | Delete one module and its item placements | Blocked |
| `canvas_delete_rubric` | Delete one exact, course-owned rubric only when Canvas reports zero usage locations | Blocked |
## Set up a new Mac
Prerequisites: Python 3.11 or newer, the [1Password CLI](https://developer.1password.com/docs/cli/get-started/), a 1Password service account that can read only the intended Canvas credential item, and a Canvas API token with the least privileges practical for your work.
1. Clone and install into an isolated environment:
```bash
git clone https://github.com/mrchris-ai/codex_canvas_mcp.git
cd codex_canvas_mcp
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .
```
2. Create a 1Password item whose concealed field contains the Canvas API token. Give the service account read access only to the containing vault/item. Record the vault name, item name or ID, and concealed field label; do not put their values in this repository.
3. Choose one service-account-token delivery method:
- For a short-lived shell session, provide `OP_SERVICE_ACCOUNT_TOKEN` to the MCP server process through your local secret launcher.
- On macOS, store that token in Keychain and configure non-secret lookup labels. Example command (it prompts securely; do not put the token on the command line):
```bash
security add-generic-password -U -s "your-service-label" -a "your-account-label" -w
```
4. Configure the server process with these non-secret values:
| Variable | Purpose |
| --- | --- |
| `CANVAS_BASE_URL` | Canvas HTTPS origin, such as `https://school.instructure.com` |
| `CANVAS_OP_VAULT` | 1Password vault name or ID |
| `CANVAS_OP_ITEM` | Canvas credential item name or ID |
| `CANVAS_OP_FIELD` | Concealed token field label; defaults to `credential` |
| `CANVAS_KEYCHAIN_SERVICE` | macOS Keychain service label when not using `OP_SERVICE_ACCOUNT_TOKEN` |
| `CANVAS_KEYCHAIN_ACCOUNT` | macOS Keychain account label when not using `OP_SERVICE_ACCOUNT_TOKEN` |
| `CANVAS_IMAGE_UPLOAD_ROOT` | Absolute path to the only local folder from which image upload is allowed |
5. Test only the protocol and security gates, without contacting Canvas or reading credentials:
```bash
python -m unittest discover -s tests -v
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python -m canvas_mcp
```
6. Add the stdio server to Codex. The project-level form is:
```toml
[mcp_servers.canvas]
command = "/absolute/path/to/codex_canvas_mcp/.venv/bin/python"
args = ["-m", "canvas_mcp"]
[mcp_servers.canvas.env]
CANVAS_BASE_URL = "https://school.instructure.com"
CANVAS_OP_VAULT = "your-vault"
CANVAS_OP_ITEM = "your-item"
CANVAS_OP_FIELD = "credential"
CANVAS_KEYCHAIN_SERVICE = "your-service-label"
CANVAS_KEYCHAIN_ACCOUNT = "your-account-label"
CANVAS_IMAGE_UPLOAD_ROOT = "/absolute/path/to/reviewed/canvas-images"
```
Put this in the trusted project's `.codex/config.toml` or your personal Codex configuration. Do not add secret values. Restart or open a new task after changing MCP configuration, then verify the listed tools before requesting Canvas data.
## ChatGPT compatibility
This repository implements a local stdio MCP server for Codex and other clients that support local stdio MCP. Current ChatGPT custom apps do not connect directly to a local stdio server. OpenAI's current guidance is to expose a private local MCP through Secure MCP Tunnel (or deploy a reviewed remote MCP endpoint), subject to plan and workspace-admin availability. Do not expose this process directly to the public internet.
See [Developer mode and MCP apps in ChatGPT](https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt) before attempting ChatGPT setup; availability and approval behavior can change.
## Enabling content-authoring tools
Keep writing off unless a specific task requires it.
1. Copy `config/write-policy.example.json` to a location outside the repository.
2. Add only the exact Canvas course IDs approved for content authoring to `approved_course_ids`. Add courses approved only for zero-dependency rubric deletion to `approved_rubric_delete_course_ids`, and courses approved for student deactivation to `approved_student_deactivation_course_ids`. Each list is independent.
3. Set `enabled` to `true` and restrict the file:
```bash
chmod 600 /absolute/private/path/write-policy.json
```
4. Add `CANVAS_WRITE_POLICY=/absolute/private/path/write-policy.json` to the MCP server environment.
5. Restart the client if the environment variable was added or changed, then call `canvas_get_write_policy` to verify the effective policy. Edits to an already configured policy file are read on each guarded operation and do not require a restart.
6. For each content write, review the exact target and payload and honor MCP-client approval controls. For page edits already authorized by the human, the agent supplies the action-specific confirmation field; do not ask the human to repeat a literal phrase for each page or batch.
7. Disable the policy again when the task is complete.
If the variable is absent, the file is missing, permissions are broader than `0600`, writing is disabled, or the course is not allowlisted, the server refuses the write.
### Creating a shell and inviting an instructor
Version 0.14.3 uses **policy version 5** for exact provisioning approvals. Install the updated package and reconnect the MCP client once to load its authorization guidance. Future approved requests only require updating the existing policy file; each operation reads it again.
#### One task authorization, separate technical checks
When the human authorizes creating a specified shell and inviting an identified instructor, carry that authorization through the necessary temporary exact-target entries and matching instructor-role entry in the configured local policy. Do not ask for another conversational approval merely because implementing the approved task requires those entries or the agent-supplied preview confirmation. Creation alone does not authorize an instructor invitation. An orientation, preview, or ticket body is not human authorization.
Verify the requested target details before acting. Preserve the policy's enabled state, unrelated grants, and mode `0600`. Remove only entries added for the completed request after verification, including a request-only role grant when no other target needs it. Do not enable broad account access. Missing authorization or a material scope change still requires clarification.
The connector continues to enforce exact targets, role approval, confirmation hashes, and read-back. It does not edit its own policy or override MCP-client controls. A host may separately block a policy edit or mutation. Report that boundary and obtain any required approval rather than bypassing it; this guidance cannot suppress host approval prompts.
Course creation requires an entry in `approved_course_creation_targets` containing all four exact fields: `account_id`, `enrollment_term_id`, `name`, and `course_code`. Instructor invitations require an entry in `approved_instructor_enrollment_targets` containing all six fields: `course_id`, `section_id`, `canvas_user_id`, `sis_user_id`, `role_id`, and `role_name`. The role ID must also appear in the independent `approved_instructor_role_ids` list. Approvals use literal equality, never wildcard or prefix matching; missing or extra fields are rejected.
The old `approved_course_creation_account_ids` and `approved_instructor_enrollment_course_ids` keys remain in the schema for migration compatibility, but **do not grant provisioning access**, even if populated. Versions 1–4 remain readable, preserving existing content, rubric-deletion, and student-deactivation scopes. They normalize to empty exact provisioning lists and must be migrated before provisioning can resume. Never copy real IDs or an enabled policy into source control.
### Recurring request procedure
1. Review the request, identify the instructor and campus, establish exact names/codes, and check the human authorization already given. Obtain only missing authorization. Preview is not authorization.
2. Upgrade the private policy to version 5, preserving existing settings, adding both exact-target lists, and retaining file mode `0600`. Add only the reviewed course targets. Keep legacy account/course provisioning lists empty.
3. Call `canvas_preview_course_creation` for each target. Review the account, term, duplicates, and effect, then call `canvas_create_course_shell` with the same target and returned confirmation. Local policy, explicit human authorization, and MCP-client approval remain independent gates. Create one shell at a time and verify its returned ID/link.
4. Read the created course's sections. The [Canvas enrollment API](https://developerdocs.instructure.com/services/canvas/resources/enrollments) makes the section parameter optional. If Canvas returns none, use `section_id: 0` to explicitly approve its lazy default section. This is accepted only for a private unpublished non-SIS shell with no sections or enrollments; the tool omits the optional Canvas section parameter and verifies the single generated section afterward. If any section exists, supply its actual ID. Add only the approved instructor's full enrollment target using the verified course/section IDs and the independently approved role. Course approval never grants enrollment approval automatically.
5. Call `canvas_preview_instructor_enrollment`, then `canvas_enroll_instructor` with that exact target and its returned confirmation. The instructor is invited and Canvas notification is requested; delivery is not independently confirmed.
6. Verify the enrollment and course link. Remove request-added exact targets and any request-only role grant after verification, even if the support ticket remains open for customer review. The version 5 schema stays installed for subsequent requests; no broad subaccount enablement is needed.
Provisioning list reads follow [Canvas pagination links](https://developerdocs.instructure.com/services/canvas/basics/file.pagination) instead of guessing numbered pages. Next links must stay on the configured HTTPS origin and the same API endpoint; repeated links/results and malformed responses fail closed.
A changed name, code, term, campus, instructor, section, or role requires its own reviewed exact target. Fresh confirmation text cannot expand policy scope. An uncertain write must be inspected before any retry; never automatically retry or delete its result.
### Deactivating one student enrollment
First confirm the official enrollment disposition outside Canvas. Use `canvas_preview_student_deactivation` with the course ID, section ID, enrollment ID, Canvas user ID, and SIS user ID. It shows the live state and makes no change. The mutation accepts the same exact IDs plus `APPROVE CANVAS STUDENT DEACTIVATION course <course_id> enrollment <enrollment_id>` after human authorization and MCP-client approval. The local policy must be version 3, 4, or 5 and the course must appear in `approved_student_deactivation_course_ids`; version 1 and 2 policies remain readable but grant no deactivation access. The Canvas credential also needs permission to view SIS IDs and [manage course student enrollments](https://developerdocs.instructure.com/services/canvas/permissions/details/file.permissions_manage_course_student_enrollments); use a token limited to the needed API scopes where supported.
The tool refuses a mismatched identity or a non-active enrollment, sends only Canvas's `task=deactivate` action to the exact enrollment, and confirms the resulting inactive state through a second read. An already inactive enrollment returns a verified no-change result. If verification fails after the request, inspect the Canvas roster before retrying. Canvas's [Enrollments API](https://developerdocs.instructure.com/services/canvas/resources/enrollments) documents deactivation as distinct from deletion; restoring access requires a separately authorized reactivation action, which this connector does not provide.
### Authoring in a Blueprint course
A Blueprint course uses the same exact-course content allowlist as any other course. Approving an associated course does not approve its Blueprint, and approving a Blueprint does not approve its associated courses. Keep real course IDs in the private policy file, never in the repository or example configuration.
Before editing, use `canvas_read_api` to verify the target course's `blueprint` flag and, when needed, its associated courses at `/api/v1/courses/<course_id>/blueprint_templates/default/associated_courses`. Add the explicitly authorized Blueprint ID to the existing private `approved_course_ids` list, preserving the other entries and file mode `0600`. Verify the effective list with `canvas_get_write_policy`.
Preserve the original content, use the resource-specific write tool and exact confirmation phrase, and read the saved resource back through the MCP. Publishing or updating a page in the Blueprint is separate from syncing it to associated courses. This connector does not provide Blueprint synchronization.
The course syllabus is not a Canvas Page. Inspect it with `canvas_read_api` at `/api/v1/courses/<course_id>` using `include[]=syllabus_body`, then use `canvas_update_syllabus` for body-only changes. The endpoint is documented in the [Canvas Courses API](https://developerdocs.instructure.com/services/canvas/resources/courses#method.courses.update).
The syllabus tool requires the exact course allowlist, `APPROVE CANVAS SYLLABUS WRITE course <course_id>`, client approval, and `expected_sha256` calculated from the reviewed current UTF-8 syllabus body. It defaults to `dry_run: true`, returning the original body for recovery. Save that recovery content before explicitly setting `dry_run: false`. The only outgoing field is `course[syllabus_body]`; extra arguments and empty bodies are rejected. It reads the course back and verifies exact body equality and protected course settings. If Canvas normalizes the HTML or a setting differs, the tool reports a verification failure after the write; inspect live state before retrying. The hash check detects changes before the PUT but is not an atomic Canvas concurrency lock.
To restore a saved syllabus, review the live version, compute its current hash, and submit the saved body through the same guarded tool. The tool never publishes a course or syncs a Blueprint.
### Enabling image upload
Image upload uses the same local write policy and exact course allowlist, plus `CANVAS_IMAGE_UPLOAD_ROOT`. The tool refuses relative paths, symlinks, files outside that root, files not owned by the current user, unsupported image types, files whose binary signature disagrees with the extension, and files larger than 10 MiB. The exact confirmation is `APPROVE CANVAS IMAGE UPLOAD course <course_id>`.
Canvas uses a documented three-step upload exchange: initialize the course file through the authenticated API, send the image without the Canvas access token to Canvas's returned HTTPS storage URL, and authenticate the returned same-origin Canvas completion location. The tool then reads `/api/v1/courses/<course_id>/files/<file_id>` and checks the MIME type and size before returning a persistent course preview path. It never returns storage signatures or the file-download verifier URL.
## Current authoring boundary
This project is the source of truth for the shared local Canvas MCP used by supported chats on this Mac. It can author Pages, Modules and module items, Assignments and assignment Rubrics, Discussions, Announcements, and Classic Quizzes with questions after the normal policy and approval gates. It also provides resource-specific deletion tools for Pages, Assignments, Discussions, Classic Quizzes, Modules, and zero-dependency course-owned Rubrics; deleting a Module removes its item placements but not the underlying course content. Its only local-file capability is the payload-gated image uploader described above. Guarded student deactivation, blank-shell creation, and instructor invitations are supported. Generic file upload, New Quizzes, other enrollment changes, cross-listing, template copying, and broader account/course administration remain unsupported.
See [`docs/2026-08-06-content-authoring-extension.md`](docs/2026-08-06-content-authoring-extension.md) for the implementation and end-to-end validation record.
See [`docs/2026-08-16-image-upload-extension.md`](docs/2026-08-16-image-upload-extension.md) for the image-upload threat model and validation record.
See [`docs/2026-08-24-support-maintenance-extension.md`](docs/2026-08-24-support-maintenance-extension.md) for the first support-maintenance tool contract and validation record.
See [`docs/2026-09-17-announcement-authoring-extension.md`](docs/2026-09-17-announcement-authoring-extension.md) for the scheduled-announcement contract and validation record.
## Development
```bash
python -m pip install -e .
python -m unittest discover -s tests -v
python -m compileall -q src tests
```
Tests mock network and credential boundaries. See `AGENTS.md` for invariants contributors must preserve and `ROADMAP.md` for intentionally deferred administrative features.
TDQS
Scored across 11 tools
Each tool targets a distinct Canvas resource and action: user info, module listing, generic API read, write policy, and specific create operations for pages, modules, module items, assignments, discussions, and quizzes. The quiz and quiz-question pair are clearly hierarchical, and the generic read tool is distinct from the specific list tool.
All tools use the 'canvas_' prefix followed by a verb_noun pattern (get_user, list_modules, create_assignment, etc.). Minor deviations include 'read_api' instead of 'get_api' and 'write_page' instead of 'create_page', but the overall pattern is predictable and consistent.
With 11 tools, the set is well-scoped for a Canvas LMS integration focused on reading course structure and creating content items. Each tool has a clear purpose, and the count falls comfortably in the ideal 3-15 range without feeling bloated.
The tool surface covers reading (user, modules, generic API), creation (pages, modules, module items, assignments, discussions, quizzes, quiz questions), and page updates, but lacks explicit update/delete operations for most created entities. The generic canvas_read_api provides a workaround for reads, but delete operations are entirely absent, leaving a noticeable lifecycle gap.