CLAUDE.md · diff

git:20260928.562eef8 to git:20260930.87393f2

53 added, 473 removed. Audit B to A.

# CLAUDE.md
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
- > **Note**: This guide is for developers working ON the Canvas MCP codebase. If you're an AI agent USING the MCP server, see [AGENTS.md](./AGENTS.md) instead.
-
- # Canvas MCP Development Guide
-
- ## Environment Setup
- - Install uv package manager: `pip install uv`
- - Install dependencies: `uv pip install -e .`
- - Create `.env` file with `CANVAS_API_TOKEN` and `CANVAS_API_URL`
- - Server installed as CLI command: `canvas-mcp-server`
+ Guidance for developing the Canvas MCP server. Agents *using* the server: see [AGENTS.md](./AGENTS.md).
+ Design: [internal/architecture.md](internal/architecture.md). Long-form rules and the reasons behind them: [internal/dev-reference.md](internal/dev-reference.md). Completed work: [internal/project-history.md](internal/project-history.md), `CHANGELOG.md`.
## Commands
- - **Start server**: `canvas-mcp-server` (or `./start_canvas_server.sh` for legacy setup)
- - **Test server**: `canvas-mcp-server --test`
- - **View config**: `canvas-mcp-server --config`
- - **MCP client config**: Update your MCP client's configuration file (e.g., `~/Library/Application Support/Claude/claude_desktop_config.json` for Claude Desktop)
-
- ## Repository Structure
- ```
- canvas-mcp/
- ├── src/canvas_mcp/ # Main application code
- │ ├── core/ # Core utilities (client, config, validation)
- │ ├── tools/ # MCP tool implementations (up to 103 tools across 20 files)
- │ ├── resources/ # MCP resources and prompts
- │ └── server.py # FastMCP server entry point
- ├── skills/ # Agent skills for skills.sh (8 skills)
- ├── tests/ # 900+ tests (pytest + pytest-asyncio)
- ├── docs/ # GitHub Pages site + guides
- ├── tools/ # Tool documentation (README.md, TOOL_MANIFEST.json)
- ├── archive/ # Legacy code (git-ignored)
- └── .env # Configuration (CANVAS_API_TOKEN, CANVAS_API_URL)
- ```
-
- ## Architecture Overview
-
- FastMCP server; type-driven validation via `@validate_params`; dual-layer course code↔ID caching; flexible identifiers (`get_course_id()`); ISO-8601 dates. Tools use a List→Details→Content→Analytics progressive-disclosure pattern, grouped by Canvas entity, named `{action}_{entity}`. All Canvas calls route through `make_canvas_request()` with async I/O, automatic pagination, and configurable anonymization.
-
- **Full design reference** (patterns, parameter validation, analytics engine, messaging system): [internal/architecture.md](internal/architecture.md).
+ - Install `uv pip install -e .`; run `canvas-mcp-server` (`--test`, `--config`); `.env` holds `CANVAS_API_TOKEN` and `CANVAS_API_URL`.
+ - Before committing: `uv run python -m pytest tests/ -v -rf`. TypeScript changes: `npm test` and `npm run build`.
## Git Workflow - ASK FIRST
-
- **Before starting any new feature or significant change, ASK:**
- > "Should I create a feature branch for this, or work directly on main?"
-
- | Change Type | Default Branch | Notes |
- |-------------|----------------|-------|
- | New tool/feature | `feature/tool-name` | PR with CI checks |
- | Bug fix | `fix/issue-description` | PR recommended |
- | Documentation only | `main` okay | Direct push acceptable |
- | Quick fix (typo, etc.) | `main` okay | Direct push acceptable |
-
- **Branch naming:** `feature/`, `fix/`, `docs/`, `refactor/`
-
- This repo has branch protection on `main` (PR + status checks required), but admin can bypass. Always ask the user which workflow they prefer for the current task.
-
- ### Parallel work: one PR = one worktree
-
- This repo often has several agents/sessions working at once. The primary checkout
- (`/Users/vishal/code/canvas-mcp`) stays on `main`, clean — treat it as read-only (triage,
- review, reading). All branch work happens in a sibling worktree named `canvas-mcp-<slug>`
- on branch `fix/NNN-slug`, created from `origin/main` (gitignored files like `.env` don't
- carry over — symlink them). Never repurpose a worktree for a different issue; remove it
- after its PR merges and delete the branch (local + remote). After any sibling PR merges,
- rebase surviving worktree branches onto `main` and rerun tests there. Full lifecycle:
- global `worktree-pr` skill.
-
- ### Closing-keyword guard — run `./scripts/install-hooks.sh` once per clone
-
- GitHub closes an issue on any `fixes|closes|resolves #N` in a merged PR body **or
- a commit message landing on `main`** — including prose that only *describes*
- other work. Issue #172 was closed twice this way (PR #202's body, then commit
- `98643ce` whose message documented the first accident).
-
- `scripts/check_closing_keywords.py` is the single detector, shared by the
- `commit-msg` hook (prevention) and `.github/workflows/closing-keyword-guard.yml`
- (backstop). It blocks keywords **mid-sentence** and allows ones that **open a
- line** — `Closes #173` is a deliberate trailer, `...bug that closed #172` is
- narration. Deliberate close on a fix PR needs no ceremony; narration must be
- rephrased (`closed [issue 172]`) or bypassed with `ALLOW_CLOSING_KEYWORD=1`.
-
- ---
-
- ## Release Checklist
-
- Version-bump procedure (files to update) + publish-race gotchas: **[internal/release-checklist.md](internal/release-checklist.md)**.
-
- ---
+ - Before a feature or significant change, ask: feature branch or `main`? Defaults: new tool `feature/`, bug `fix/`, docs or typo may go straight to `main`. Prefixes: `feature/`, `fix/`, `docs/`, `refactor/`.
+ - The primary checkout stays on `main`, clean and read-only. One PR = one sibling worktree `canvas-mcp-<slug>` from `origin/main`; never reuse it for another issue; remove it and the branch (local and remote) after merge.
+ - After a sibling PR merges, rebase surviving worktree branches and rerun tests. Before tagging, run the suite in the primary checkout on the merged commit (its `.env` sets a token that worktrees and CI lack).
+ - `main` requires a PR plus `test-enhancements` and `lint`; the admin can bypass the review requirement.
+ - Run `./scripts/install-hooks.sh` once per clone. `fixes|closes|resolves #N` mid-sentence in a commit or PR body closes the issue on merge: rephrase (`closed [issue 172]`) or set `ALLOW_CLOSING_KEYWORD=1`. A trailer that opens a line (`Closes #173`) is allowed.
+ - Release steps and publish-race fixes: [internal/release-checklist.md](internal/release-checklist.md). Breaking changes need a minor bump.
## Coding Standards
- - **Type hints**: Mandatory for all functions; use PEP 604 unions (`X | Y`, `T | None`), which ruff UP enforces
- - **MCP tools**: Use `@mcp.tool()` decorator with `@validate_params`
- - **Async functions**: All API interactions must be async
- - **Course identifiers**: Use `str | int` and `get_course_id()` for flexibility
- - **Date handling**: Use `format_date()` for all date outputs
- - **Error responses**: dict-returning tools include an `"error"` key; string-returning tools return a human-readable `"Error ..."` message (match the module you're editing)
- - **Legacy `-> str` tools**: a few modules (notably `modules.py` and `accessibility.py`) still return JSON-stringified error objects instead of plain `"Error ..."` text; preserve the local convention when editing them
- - **Form data**: Use `use_form_data=True` for Canvas POST/PUT endpoints
- - **Privacy**: Student IDs preserved, names anonymized in `_should_anonymize_endpoint()`
+ - Type hints on every function; PEP 604 unions (`X | Y`), enforced by ruff UP.
+ - Tools use `@mcp.tool()` with `@validate_params`, and every tool carries annotations (`tests/test_tool_metadata.py` fails a bare decorator).
+ - All Canvas calls are async and go through `make_canvas_request()`; paginate every list endpoint.
+ - Course identifiers are `str | int` resolved with `get_course_id()`; dates go out through `format_date()`.
+ - Canvas POST/PUT needs `use_form_data=True`, `/conversations` included.
+ - Errors: dict tools return an `"error"` key; string tools return `"Error ..."`. `modules.py` and `accessibility.py` return JSON-stringified errors; keep the local convention.
+ - Privacy: student IDs preserved, names anonymized at the client layer (`_should_anonymize_endpoint()`); a new endpoint must be checked against the tier rules.
+ - Show course codes, not IDs, in user-facing output; handle published and unpublished states.
## Testing and behavioral evidence
-
- Choose verification by the behavior at risk, not a per-tool test quota. New tools
- must have meaningful automated coverage before they are complete. Cover relevant
- success, failure, boundary, and safety behavior; several scenarios may fit one
- parameterized test, while a consequential invariant may need several kinds of
- evidence. Preserve existing regression coverage and required CI checks.
-
- ### TDD and refactoring
-
- - For a reproducible bug or a clear behavior change, write a focused failing
- regression first. Observe it fail for the intended reason, then repair the
- implementation and verify it passes.
- - Before refactoring, characterize behavior where the existing tests leave a
- meaningful uncertainty. Preserve the public contract and separate intentional
- behavior changes from structural cleanup.
- - When expected behavior is still being discovered, exploration is appropriate.
- Establish the intended contract and meaningful verification before claiming
- completion; do not turn exploratory output into its own test oracle.
-
- ### Choose the test boundary
-
- Use unit tests for local rules, integration tests for collaboration and transport
- contracts, and end-to-end tests for complete user workflows when that is the
- uncertainty. There is no universal ratio or preferred layer simply because an AI
- agent writes the code. Keep the real components involved in the risk, replacing
- external services with controlled responses where useful. End-to-end coverage
- can expose integration failures; focused tests make edge cases and failures
- cheaper to reproduce. Do not send live Canvas writes merely to verify a patch.
-
- Derive assertions from an independent requirement or hand-checked example.
- Assert observable outputs, outgoing request contracts, and forbidden side effects.
- Scrutinize mocks that remove the interaction being tested or return only the
- shape the implementation happens to expect. A passing success substring or a
- mock assertion alone rarely establishes the whole behavior.
-
- For example, this is adapted from `TestMarkConversationsRead.test_sends_form_data`
- in `tests/tools/test_messaging.py`, the form-encoding regression for #208. It checks
- the actual tool's outgoing contract:
-
- ```python
- # get_tool_function is the registration helper in that test module.
- @pytest.mark.asyncio
- async def test_marks_conversations_read_with_canvas_form_fields():
- with patch(
- "canvas_mcp.tools.messaging.make_canvas_request", new_callable=AsyncMock
- ) as request:
- request.return_value = [{"id": 319, "workflow_state": "read"}]
- tool = get_tool_function("mark_conversations_read")
- result = await tool(conversation_ids=["319"])
-
- assert result["success"] is True
- assert request.call_count == 1
- assert request.call_args.kwargs["use_form_data"] is True
- assert request.call_args.kwargs["data"] == {
- "conversation_ids[]": ["319"], "event": "mark_as_read"
- }
- ```
-
- This checks the tool-to-client boundary. Use a real client with controlled HTTP
- transport when the uncertainty is serialization, retries, pagination, or error
- classification; this mocked example does not establish those properties.
-
- ### Formal methods and completion gates
-
- Use Lean/TLA+ selectively for consequential state, ownership, concurrency, or
- termination invariants. Keep the source-to-model mapping and assumptions explicit,
- and pair model proofs with implementation regressions. A proved model does not
- prove the HTTP service, compiler, or every caller follows it. See `verify/` for
- scoped examples and pinned toolchain instructions.
-
- Run focused checks during iteration and the full Python suite before committing:
- `uv run python -m pytest tests/ -v -rf`. Run `npm test` and `npm run build` for
- TypeScript changes, and relevant formal checks when their implementation mapping
- changes. Keep required lint, type-checking, and CI gates; do not merge with failing
- required checks. Report what was verified and any failures, skipped coverage, or
- environment limitations. Do not weaken assertions to obtain a green run.
-
- ## Canvas API Specifics
- - Base URL from `CANVAS_API_URL` environment variable
- - Authentication via Bearer token in `CANVAS_API_TOKEN`
- - Always use pagination for list endpoints
- - Course codes preferred over IDs in user-facing output
- - Handle both published and unpublished content states
- - **Messaging requires form data**: Use `use_form_data=True` for `/conversations` endpoints
- - **Privacy protection**: Real user IDs preserved for functionality, names anonymized for privacy
+ - New tools need meaningful automated coverage: success, failure, boundary and safety behavior.
+ - A reproducible bug gets a failing regression first; watch it fail for the intended reason.
+ - Assert outgoing request contracts and forbidden side effects, from an independent requirement, not from the implementation's own output.
+ - Use a real client with controlled HTTP transport when the risk is serialization, retries, pagination or error classification.
+ - Never send live Canvas writes to verify a patch. Never weaken an assertion to get a green run.
+ - Lean/TLA+ (`verify/`) only for consequential state, ownership or termination invariants; pair with implementation regressions.
+ - Report what was verified, what failed and what was skipped.
## Documentation Maintenance
+ - Source of truth: agents `AGENTS.md`; humans `tools/README.md`; machine `tools/TOOL_MANIFEST.json`; `README.md` is the entry point.
+ - New tool: update `tools/README.md`, then `AGENTS.md`, then `TOOL_MANIFEST.json`. Touch `README.md` only for a major feature. No tool usage docs in this file.
+ - `docs/` is the public Cloudflare Pages root and deploys manually (`wrangler pages deploy docs/`); nothing internal goes there.
+ - `internal/` is deny-by-default in `.gitignore`; un-ignore only a file meant to be public. `internal/session-history.md` stays untracked.
+ - Triage briefs are tracked: never record a collaborator's affiliation, evaluation status, timeline or competing products. Name the person and the technical issue only.
- **Source of truth per audience:**
- - **AI agents**: `AGENTS.md` (tool tables, workflows, constraints)
- - **Humans**: `tools/README.md` (full tool docs with all params)
- - **Machine**: `tools/TOOL_MANIFEST.json`
- - **Entry point**: `README.md` (installation, overview — update on major releases only)
+ ## Hosted Deployment
+ - A private, Entra-gated Azure instance serves Gies course staff. Its URL, app IDs, deploy details and key holders stay out of this repo; they live in the gitignored `internal/ops-hosted.local.md`.
+ - HTTP mode: each caller sends `X-Canvas-Token`; `CANVAS_API_TOKEN` must never be set (startup guard); read-only unless `ALLOWED_WRITE_TOOLS` is set.
+ - Merging to `main` does not deploy production. It deploys on a `v*` tag or `gh workflow run deploy-prod.yml --ref main`; `staging` deploys on push.
- **When adding a new tool**, update: `tools/README.md` → `AGENTS.md` → `TOOL_MANIFEST.json`. Do NOT update `README.md` unless it's a major feature. Do NOT duplicate tool usage docs in `CLAUDE.md` (architecture only).
+ ## Adoption numbers
+ - Never print a PyPI download count. Quote stars, forks and contributors, re-pulled from the GitHub API that day, with the date.
+ - Do not repeat "over 18,000 clones" or "UMich selected it as sole candidate" without a primary source.
+ - Do not imply a campus security review: the only Illinois artifact is Adam King's LRA, and the public hosted server is retired.
## Current Focus
- - [x] Release v1.3.0 — `create_rubric` (#100), `read_course_file` (#90), event-loop fix (#99), bulk-delete safety (#96); tool count 88 → 90; CHANGELOG.md added
- - [x] Follow-up: split publish-mcp.yml into separate PyPI + MCP Registry jobs with PyPI-propagation poll (PR #107)
- - [x] Follow-up: add `ruff`/`black`/`mypy` to dev deps in pyproject.toml; remove unused `requests`; `setup-python@v4 → @v6` (PR #105)
- - [x] Retired public hosted server (`mcp.illinihunt.org`) — security teardown + cleaned all references (memory, website, README/AGENTS/CHANGELOG)
- - [x] Issue #115: Gies/Azure hosted deployment — **DONE 2026-06-17.** v2 Entra platform-auth (#125) + a private custom domain (bound + managed cert; URL in gitignored `docs/ops-hosted.local.md`) **resolves the `AADSTS9010010` mcp-remote blocker — verified live, all clients work.** App renamed `gies-canvas-mcp` → `canvas-mcp` (house-consistent; old apps deleted). Branch→slot CI added (#128/#129). Remaining polish: tighten `MCP_ENTRA_ALLOWED_OIDS`; AcrPull RBAC fix (needs Adam, still on ACR admin-user creds)
- - [x] PR #126: `check_enrollment` capability — **merged + shipped in v1.4.0.** Deferred: REST endpoint + teacher-token-sourcing decision
- - [x] Claude Desktop Extension (`.mcpb`) — scaffolded, distributed via GitHub Releases (auto-attached on tag), README install section; shipped in v1.4.0
- - [x] Release **v1.4.0** — GitHub + PyPI + MCP Registry + hosted server + website all live
- - [x] PR #150: self-service access-approval flow for the hosted server — merged 2026-07-01
- - [x] PR #155: `update_discussion_topic` (#154) — **merged 2026-07-04** (32152e8); #154 closed; auto-deployed to hosted
- - [x] Release **v1.5.0** (2026-07-05) — 3 new tools (93 total), fastmcp 2.x, security hardening (#156); all channels live (GitHub/PyPI/MCP Registry/hosted/site)
- - [x] Issue #159: mcp-remote proxy hangs on stale hosted session — **fixed 2026-07-09** (PR #160: `stateless_http=True`; deployed + live-verified)
- - [x] Issue #164 / PR #165: FERPA anonymization bypass (safe-endpoint short-circuit) — **fixed, merged, deployed 2026-07-21**; follow-up #166 filed
- - [x] Issue #166: anonymizer recursive identity scrub — **fixed, merged (PR #177), deployed to hosted 2026-07-29**; follow-up #179 (layer consolidation)
- - [x] **#170 Tier 1 student write tools — MERGED to main 2026-07-30 (PR #185)**, deploying with
- v1.6.0. 10 codex rounds to clean; policy carrier is the course syllabus (page carrier deliberately
- removed). Hosted instance verified write-free (CANVAS_ROLE=educator + STUDENT_WRITE_TOOLS unset;
- policy recorded in internal/ops-hosted.local.md). **#170 CLOSED 2026-08-19** as completed for the
- delivered Tier 1 work; UMich's two pilot questions (default posture; syllabus visibility) were never
- answered and are no longer gating. Design record: `internal/issue-170-followup-draft.md`
- - [x] **#171 identity tools — MERGED (PR #183)**; #171 closed. check_enrollment now returns
- INDETERMINATE instead of a confident false NO on permission-stripped rosters
- - [x] **#180 rubric visibility — MERGED (PR #182)**; #180 closed. Course-bookmark association +
- never report success on an orphaned rubric
- - [x] **#179 gap-closure half — MERGED (PR #184)**: anonymization tiers (full/identity/free_text);
- /conversations + /pages gated (live replay: 97 inbox records, 0 surviving emails); missed email
- keys covered; anonymization-map tool fixed. **#179 CLOSED 2026-08-01** — the tool-layer call
- consolidation shipped in PR #211 (plus a ruff TID251 ban to keep it consolidated)
- - [x] Release **v1.6.0** (2026-07-30) — **all five channels live + verified**: GitHub Release (+`.mcpb`),
- PyPI, MCP Registry (`isLatest=True`), site (wrangler-deployed, 1.6.0 / **96 tools**), hosted Azure.
- Behavior change in the notes: `execute_typescript` is now opt-in (#178)
- - [x] #181 `associate_rubric` never attached the rubric — **fixed + live-verified on production Canvas**
- (PR #189); shared `rubric_association_id()` / `unconfirmed_write_warning()` guard now used by every
- rubric write, closing a latent hole in the #180 bookmark path
- - [x] #186 ruff in CI (**first outside contribution**, @w3lld1) — `lint` is now a required check; #175 closed
- - [x] #188 `claude-review` could never pass on a fork PR (GitHub withholds secrets) — **dropped from
- required checks**, so external contributions are mergeable again. Required: `test-enhancements` + `lint`
- - [x] #190 `create_rubric_from_csv` — documented CSV format was **wrong** (created zero rubrics); fixed
- in #195/#196 along with `succeeded_with_errors` handling and `error_data` surfacing
- - [x] #192 `/api/quiz/v1` client routing (#193) + paginated `api_root` (#197), anonymization gate intact
- - [x] **All three zqian bugs CLOSED 2026-07-31.** **#199** was three defects with one root cause (a
- confident negative on an unchecked premise): `login_id` assumed to be the bare campus ID (measured
- live — UIUC stores `vishal`, email-provisioned instances store `uniqname@umich.edu`); an email-form
- identifier rejected by the input guard before any Canvas call; and `role`'s `student` default pushed
- to Canvas as `type[]`, hiding every other role. Adds an **AMBIGUOUS** answer for anything
- unverifiable (PR #203). **#198** fixed + measured A/B: omitting `parent_folder_path` isn't "root",
- Canvas creates an `unfiled` folder (PR #203). **#200** annotations (PR #201, Copilot agent)
- - [x] **#204 tool-annotation contract complete + CI-gated (PR #205)** — `destructiveHint` now follows
- the MCP spec ("only additive updates") instead of "destructive == deletes"; `idempotentHint` set
- everywhere and judged on **whole effect** (grade writers append a comment; page tools re-notify;
- `delete_announcements_by_criteria` re-derives its target set). `tests/test_tool_metadata.py`
- enumerates the live registry **with every feature flag on**, so a bare `@mcp.tool()` fails CI —
- the default set had hidden `execute_typescript` shipping unannotated. Convention in
- `internal/architecture.md`
- - [x] **Hosted deployment spec public (PR #206, 2026-07-31)** — `deploy/azure/` (spec + 4 placeholdered
- templates) is canonical; corrected HTML copies emailed in-thread to UMich (zqian) + UC Irvine
- (VC Choudhary); site callout live on canvas-mcp.illinihunt.org. Their feedback lands as edits to
- `deploy/azure/README.md` (`internal/hosted-spec-draft/` is scratch)
- - [x] Release **v1.7.0** (2026-08-08) — all five channels live + verified. Correctness release:
- unconfirmed-write guards (#219/#220/#221), Planner-API upcoming assignments (#222), annotation
- contract (#204), `cryptography` CVE, anonymization consolidated to the client layer (#179)
- - [x] **Security scan remediation — MERGED (PR #251, 11 commits by boundary).** 12 findings, 11
- fixed: host-filesystem boundary in the file tools (both high), a **measured** `/submissions/self`
- authorization bypass via path delimiters, CSV formula injection, Registry anonymization default,
- unauthenticated route limits, sandbox fail-closed, Canvas token at rest/in transit, AI workflow
- least privilege. Two Codex rounds (round 1 found a P1 in my own HTTPS fix; round 2 clean).
- **Three breaking changes now on main — next release needs a minor bump.**
- - [ ] **#157 sandbox egress is only mitigated, not closed.** `--network=none` is passed when
- outbound is blocked *and* the allowlist is empty — but blocking auto-allowlists the Canvas host,
- so in any working config egress falls back to the in-process Node guard, which `child_process`
- and bundled utilities bypass while `CANVAS_API_TOKEN` is in the environment. Now warns honestly
- instead of implying enforcement. Real fix needs an egress proxy or network namespace
- - [x] **#249 npm setup wizard retired — CLOSED (PR #257, 2026-08-10).** Deprecated the npm package
- (name retained), removed `cli/` + orphaned `docs/workshop.html`; restored the UIUC KB-150325 token
- link into both docs guides (it had lived only on the deleted workshop page)
- - [x] Closing-keyword guard (#231) + three bypasses closed after an independent red-team (#241).
- Contributors run `./scripts/install-hooks.sh` once per clone
- - [x] **CI never ran the test suite (PR #247)** — the *required* `test-enhancements` check looked
- for `tests/test_discussion_enhancements.py` (does not exist) and echoed a hand-written
- "✅ Basic Validation Completed / PASSED". Only **363 of 1091** tests ran on a PR (`tests/security`
- via a different workflow); **728 never ran**. Now a 3.10/3.11/3.12/3.13 matrix runs `pytest tests/`,
- with an aggregator keeping the required job *name* (a bare matrix publishes `test (3.10)`, which
- would leave the required check pending forever and block every PR). `publish-mcp.yml` no longer
- swallows failures with `|| echo "No tests found"`. **This is the likely reason so many defects
- landed green** — see the four below, each of which the suite was asserting as correct
- - [x] **#238 announcements vs discussions (PR #242)** — `include[]=announcement` is a measured no-op
- (live A/B: identical 19 topics with and without it); `only_announcements` is the real filter and
- *switches* scope rather than widening, so combining costs a second call. README + manifest
- documented a **parameter that does not exist**. `list_announcements` was educator-only while
- AGENTS.md called it shared — resolved by making it **shared** (an independent Codex run framed it
- as a registration bug where I had framed it as a docs bug, and was right). The reporter's suggested
- `/announcements?context_codes[]` was measured and rejected: returns 0 (default date window)
- - [x] **#233 page media (PR #246)** — `get_page_details` stripped `<img>`/`<iframe>` with a naive
- regex, destroying media with no trace, then labelled it "Content Preview". Measured on a real page:
- 4 embedded videos → 0. Adds `extract_embedded_media()`; both lossy steps now announce themselves;
- naive regex → `strip_html_tags` (which also drops `<script>` *contents*)
- - [x] **#234 notify_of_update (PR #245)** — measured live: Canvas's PUT returns 16 keys and none is
- `notify_of_update`, so it can never be confirmed. Now warns instead of claiming success, with a
- confident *no* for the two visible suppression cases (unpublished, <1min old)
- - [x] **#235 grade comments (PR #248)** — not a server default, but **our own artifacts taught it**:
- the bulk-grading skill shipped `comment: "Graded via automated review"` and every README grading
- example paired a comment with a grade. Also fixed: the dry run never named the comment (the
- documented safety net hid the one irreversible side effect), and the simple path used membership
- while the rubric path used truthiness, so `comment: None` posted
- - [x] **`/front_page` was ungated (PR #244)** — returned `last_edited_by` (display name, pronouns,
- avatar) while `/pages/{slug}` was gated at `identity`; the tier rule matched the `pages` path
- segment, which `front_page` lacks. **Two tests asserted the gap as correct.** Same class as #164/#179
- - [x] **#239 prompt-injection boundary — IMPLEMENTED + MERGED (PR #258, 2026-08-10).** 11 Codex
- rounds; fencing at the tool output-formatting boundary (both forms), write-marker backstop, and
- `write_confirmation` tokens making 4 fan-out senders two-step. ReDoS + token-DoS found and fixed
- along the way; live-verified. **4 breaking changes → next release is a minor bump.** #239 stays
- OPEN for 2 low-risk deferrals (course names, own profile); durability follow-up = **#262** (CI
- guard). Full record: [[project-239-untrusted-content-boundary]]
- - [x] **#318 delete confirmation — SHIPPED (PR #330, 2026-08-29)**; **#325 `ACCESSIBILITY_CHECKERS` — SHIPPED (PR #329)**; **#315 drop 3.10 — SHIPPED (PR #331), closed**. All in **v1.12.0, staged on main, unreleased** (breaking; see CHANGELOG Breaking block)
- - [x] **PR #317** non-root sandbox — **MERGED `b4f21b4` 2026-08-30** (stdin delivery + import-regression fix pushed to the fork; CodeQL 145/146 dismissed; #157 stays open); **#336 `--read-only` shipped same day (PR #339, `5bed8d5`)**; follow-up **#338** (configurable sandbox uid)
- - [ ] **#236** OAuth2 developer-key flow (from discussion #229) — additive path only, blocked on
- admin access to pilot a scoped key
- - [x] Release **v1.8.0** (2026-08-09) — all five channels live + verified. Security release:
- the 11 scan fixes + 3 breaking changes (HTTPS-only, stdio-only file tools, no-overwrite
- downloads) + #255 dependency floors/workflow least-privilege. `uv.lock` now on the release
- checklist; `cli/package-lock.json` drift fixed
- - [x] Release **v1.9.0** (2026-08-10) — all five channels live + verified. The #258 breaking
- changes (4 two-step fan-out senders) + provenance fencing + OSSF Scorecard/supply-chain work.
- **First release with `.mcpb` SLSA provenance** (`gh attestation verify` passes); the
- restructured two-job `create-release.yml` survived its first live run; no PyPI propagation race
- - [x] **#252 diagnosed (not merged as reported)**: PR #253's form-data fix measured unnecessary —
- wire encodings equivalent; likely the pre-#220-guard permission failure on v1.6.0. Awaiting
- zqian's retest on v1.7.0+; #253 open pending that
- - [ ] **PR #191 (Copilot) quizzes BLOCKED on correctness** — note this is a *PR* against issue
- **#172**, not an issue itself. New Quizzes detection is `is_quiz_assignment AND external_tool`, but
- measured live that flag marks *Classic* quizzes — the `AND` may match nothing and silently report zero
- New Quizzes. Its test fixture hard-codes the assumption. Unblocking needs zqian's **scoping question 4**
- (a New-Quizzes-enabled sandbox). Two more blockers: a 262-line non-mechanical conflict in
- `assignments.py`, and a live `Fixes` line in the PR body that would auto-close #172 on merge (#172 has
- already died this way twice). **PR #191 CLOSED 2026-08-26** as unverifiable; #172 stays open; reopen when a sandbox exists
- - [x] Daily triage routine live (`trig_011HVR6j4c5hDR2fj7k3ujxC`, 7am local) — #202 merged. **Prompt
- patched 2026-07-31**: merging a brief closed #172, because it described another PR as `fixes #172`
- and GitHub parses closing keywords anywhere in a merged PR body. Routine now forbids them *and*
- greps its own output before opening the PR (#172 reopened)
- - [x] Issue #142 → FastMCP 4 / MCP SDK 2 migration complete: dependency
- constraints and lockfile upgraded, protocol-model reads use native snake_case,
- and CI runs with the temporary camelCase compatibility bridge disabled
- - [x] Issue #145 / PR #167: fastmcp 3.4.4 migration — **DONE 2026-07-21** (CVEs PYSEC-2026-2475/2476 resolved; dep-scan green; staging-validated then prod-deployed + live-verified; #145 closed)
- - [ ] Issue #157: `execute_typescript` sandbox hardening backlog (container-level egress, non-root user, prebuilt tsx image) — **self-hosted-only now**: tool is DISABLED on both hosted slots (`EXECUTE_TYPESCRIPT_ENABLED=false`, verified 2026-07-10); gate on re-enabling hosted code-exec
- - [ ] **Agent Plugins ([agent-plugins.org](https://agent-plugins.org)) → watch item, no owner.** Spec 1.0.0
- landed 2026-08-06 (TSC: Amazon, Cursor, Microsoft, OpenAI, Vercel): root `plugin.json` +
- `skills/<name>/SKILL.md` + `mcp.json`. Our `skills/` is **already conformant**, so packaging is ~2
- files / ~30 min. **Blocked on credentials, not packaging:** the spec "defines no portable OAuth or
- credential-reference fields", `mcp.json` `env`/`headers` are literal visible package data (only
- `${PLUGIN_ROOT}` / `${PLUGIN_DATA}` expand), and the subprocess base environment is *client-selected*
- — so `CANVAS_API_TOKEN` + `CANVAS_API_URL` have no delivery path and a plugin install would fail on
- first tool call. Strictly worse than the `.mcpb` (keychain prompt) we already ship. **HTTP side does
- not apply:** hosted instance is private (URL stays out of this repo) and per-caller `X-Canvas-Token`
- can't live in portable headers. Audience skew too — Claude Code uses its own `.claude-plugin/plugin.json`,
- Anthropic isn't on the TSC, and skills.sh already covers 40+ agents. Triggers to revisit: (1) a client
- ships user-secret prompting for plugin MCP servers, or the spec adds credential refs; (2) Claude
- clients adopt/bridge the format (both layouts use `skills/<name>/SKILL.md`, so dual-shipping is cheap);
- (3) a user files an issue. If ever built: `cwd: "${PLUGIN_DATA}"` + a setup skill writing `.env` there
- is the viable pattern, but needs an explicit path in `load_dotenv()` (it resolves against the calling
- module, not CWD). Cost if adopted: a 4th version-stamp location in the release checklist
- - [ ] Backlog triage (module templates, bulk creation, page versioning — feature ideas only, no owner)
- - [x] Issue #106: mypy 229 → 0 errors + mypy in CI lint job (PR #213, 2026-08-01)
- - [x] **#275 `get_my_peer_reviews_todo` — CLOSED 2026-08-20** on khagyard's confirmation
- ("The fix worked thank you!"). PR #288's Planner-feed discovery path is what fixed it; the
- assignment-scoped `peer_reviews` endpoints are instructor-focused, which @aesse97 called
- correctly in the thread. Two corrections to the old note here: the earlier claim that khagyard
- "confirmed still not found even with the direct lookup" is **unsupported** — their report
- predates any build containing PR #277's `assignment_identifier`, and they never answered which
- version they were on. And the **root cause of the original discovery-scan miss was never
- diagnosed, only routed around**. Their production payload is now the acceptance-replay fixture
- - [x] **#309 content migration — PR #316 MERGED 2026-08-20** (`ea50c711`). Two educator-only tools:
- preview→confirm course copy + one-poll-per-call status with migration-issue review. **#309 stays
- OPEN** for zqian's answer on `selective_import` (deliberately out of v1: a second async workflow
- that cannot be measured without a sandbox) and for a real sandbox payload — the bracket-form
- encoding, Progress state vocabulary, and migration-issue field names are doc-derived, not measured
-
- - [x] **#283 announcement→discussion silent fallback — two-layer fix complete (PR #285 +
- PR #291, merged 2026-08-14).** jonespm's retest showed the deeper mechanism: Canvas answers
- 200 to a student's create_announcement, silently drops `is_announcement`, and creates a real
- discussion topic. PR #291 adds (1) a permission pre-check — `GET /courses/:id?include[]=permissions`,
- measured live: flags exist ONLY on the single-course endpoint (list ignores the include;
- `/permissions` omits them); refuses only on explicit `false`, fails open otherwise — and
- (2) cleanup: the orphaned topic is auto-deleted on downgrade detection. Two opencode rounds
- (round 1 found a None-body TypeError on the cleanup DELETE; round 2 APPROVE). Issue stays
- open for khagyard's student-token retest from main (their test course still has orphan topic 674). **#283 CLOSED 2026-08-26** on khagyard's confirmation
- - [x] **#281 search_canvas_tools never searched MCP tools — fixed (PR #286, merged 2026-08-13).**
- It searched only code_api TS files (bruchris's outside diagnosis, correct). Now also
- queries the live registry (`mcp.list_tools(run_middleware=False)`) with labeled sections;
- **breaking: response shape v2** (`schema_version: 2`, flat `tools` key gone), shape pinned
- by test. Follow-up #287 filed (pre-existing uncapped `full`-mode TS dumps). zqian confirmed
- on `main` (multiple queries) — **issue CLOSED 2026-08-14**
- - [x] **#287 uncapped full-mode TS dumps — CLOSED (PR #290, merged 2026-08-14).** Second
- outside code contribution (@SHIL0018): 2,000-char cap + regression test on the discovery
- code-API full branch. Fork CI needed manual approve-runs; `claude-review` failed as always
- on forks (not required). Verified the fixture can't pass vacuously (matched file is 18.8KB)
- - [x] **#270 isError + #271 double-payload — IMPLEMENTED AND MERGED 2026-08-19** (commits `2f87e13`,
- `85b1f16`, `feae3a8`; new `src/canvas_mcp/core/tool_results.py`). Both issues CLOSED. Tool failures now
- set MCP `isError: true`; string-returning tools no longer duplicate their value into
- `structuredContent.result`. **Two breaking wire-shape changes sitting UNRELEASED on `main`** — see the
- release note below
- - [x] **#262 CI fencing guard — DONE 2026-08-19** (`b9c93a5`, registry-wide read-tool fencing coverage);
- #262 CLOSED. This was the durability follow-up named on #239, which is also CLOSED (2026-08-19) — its
- two low-risk deferrals (course names, own profile) are documented policy choices now, re-file narrowly
- if ever wanted
- - [x] Release **v1.11.0** (2026-08-20) — all five channels live + verified: GitHub Release
- (`.mcpb` + SLSA, `gh attestation verify` exit 0), PyPI 200, MCP Registry `isLatest=True`,
- site wrangler-deployed to the custom domain, hosted Azure auto-deployed (401 challenge healthy).
- Protocol-correctness release: #303 rename (breaking), #270 `isError`, #271 double-payload,
- fastmcp 3.4.7 floor. **Registry job failed once on a NEW failure mode** — an unauthenticated
- `api.github.com` lookup rate-limited to `null`, surfacing as `not in gzip format`; a rerun
- fixed it and the step is now authenticated with a null guard (`4639847`). Not the PyPI race:
- PyPI already returned 200. Both modes and the test that distinguishes them are in the checklist
- - [x] Release **v1.13.0** (2026-09-27): GHSA-hmr8 fix (`ALLOWED_WRITE_TOOLS`, two-step `send_conversation`, read-only `get_conversation_details`, syllabus token fixes), production deploys only on release tags (#417). All five channels verified. Advisory still to publish
+ - [ ] **#157** sandbox egress is mitigated, not closed (self-hosted only; `execute_typescript` is disabled on hosted). Needs an egress proxy or network namespace; also #338 (configurable sandbox uid).
+ - [ ] **#236** OAuth2 developer-key flow: additive only, blocked on admin access to pilot a scoped key.
+ - [ ] **#172** Canvas Quizzes tools: blocked on a New-Quizzes-enabled sandbox (PR #191 was closed as unverifiable).
+ - [ ] **#418 to #421** (raw dates, guarded edits, honest pagination, anonymous discussions); #420 includes an `assign_peer_review` placeholder write past 100 submissions.
+ - [ ] GHSA-hmr8: publish and request a CVE. GHSA-7pp5: decide (in triage since 07-04).
+ - [ ] Watch, no owner: Agent Plugins packaging (blocked on credential delivery; triggers in project-history).
## Roadmap
- - [ ] Grok/xAI integration research and pilot — verify the current official path for third-party
- tools, connectors, bots, or MCP servers; document registration, hosting, authentication, review,
- and cost requirements; then test the nearest supported integration path. If Grok still has no
- public plugin or MCP registry, record that boundary and revisit when xAI publishes one.
- - [x] Release v1.0.8 — all CI/CD pipelines passing (PyPI, MCP Registry, GitHub Release)
- - [x] Learning Designer tools & skills — `get_course_structure` tool + 3 skills (QC, accessibility, builder)
- - [x] GitHub Pages audit — 7 disconnects fixed (tool count, test count, analytics, URLs, compatibility)
- - [x] MCP token optimization — trimmed tool docstrings ~35% (350 lines removed across 15 files)
- - [x] HTTP transport & hosted server — per-request credentials via ContextVar. VPS instance (mcp.illinihunt.org) **decommissioned 2026-06-05** (workshop-only; public code-exec surface); Gies/Azure rebuild tracked in issue #115
- - [x] Cloudflare Pages migration — site moved from GitHub Pages (blocked by Actions) to Cloudflare Pages
- - [x] Release v1.2.0 — role-based filtering, accessibility remediation, security hardening, contributor acknowledgements
- - [x] Release v1.3.0 — create_rubric, read_course_file, event-loop fix, bulk-delete safety, CHANGELOG.md
+ - [ ] Grok/xAI integration research and pilot.
## Backlog
- - [x] Impact tracker: automated weekly stats collection + website section
- - [ ] Module templates (pre-configured module structures)
- - [ ] Bulk module creation from JSON/YAML specs
- - [ ] Module duplication across courses
- - [ ] Page templates
- - [ ] Bulk page creation from markdown files
- - [ ] Page content versioning/history tools
- - [ ] **2026-09-06 security review follow-ups** (Codex xhigh): the branch merged as PR #360 on 2026-09-12 (shared-server exports refused over HTTP, logged URL credentials redacted). Check `docs/superpowers/plans/2026-09-06-security-review-followups.md` for items #360 did not cover before closing this.
-
- ## Hosted Deployment (Azure — #115)
-
- There is a **private, Entra-gated** hosted instance for Gies course staff. It is **not shared
- publicly** — keep its endpoint URL, Entra app IDs, deploy specifics, and access-key holders out
- of this (public) repo. All operational detail lives in the **gitignored** `internal/ops-hosted.local.md`
- (moved out of `docs/` on 2026-06-21 — that dir is the Cloudflare Pages publish root and was serving
- these local-only files publicly; `docs/.assetsignore` is now a backstop).
-
- - **Architecture (no secrets):** Azure App Service (Web App for Containers) inside the UIUC
- `urbana-business-disruptionlab` subscription, fronted by App Service Easy Auth in API/bearer
- mode (Entra platform auth, RFC 9728 PRM + `401` challenge). The app reads the trusted
- `X-MS-CLIENT-PRINCIPAL-ID`; each caller passes their own `X-Canvas-Token`; the Canvas URL is
- server-pinned; `CANVAS_API_TOKEN` must never be set in HTTP mode (startup guard). Deploy is
- GitHub Actions: `deploy-staging.yml` on push to `staging`; `deploy-prod.yml` only on a `v*`
- release tag or a manual run from `main`. **Merging to `main` does not deploy production**
- (since 2026-09-27); ship unreleased `main` with `gh workflow run deploy-prod.yml --ref main`.
- - The open-source **self-hosted (stdio)** path is the public product — see `README.md` / `AGENTS.md`.
- HTTP-transport env-var *names* live in `env.template` / `core/config.py`; the hosted *instance*
- is operator-only.
+ - [ ] Module templates; bulk module creation from JSON/YAML; module duplication across courses.
+ - [ ] Page templates; bulk page creation from markdown; page versioning tools.
+ - [ ] 2026-09-06 security review follow-ups: check `docs/superpowers/plans/2026-09-06-security-review-followups.md` for items PR #360 did not cover.
## Session Log
- > Full history: `internal/session-history.md` — **local-only, untracked since 2026-08-20**
- > (it carried a paraphrase of a collaborator's private email and the private hosted endpoint
- > URL while being world-readable). Do not re-add it to git.
-
- > **`internal/` is deny-by-default in `.gitignore`.** Add an un-ignore only for a file
- > deliberately meant to be public. Daily triage briefs stay tracked because the routine reads
- > the newest one to compute its cutoff, so they **must not** record an external collaborator's
- > institutional affiliation, evaluation status, deployment timeline, or which competing
- > products they are weighing. Name the person and the technical issue, nothing else.
+ > Full history: `internal/session-history.md`, local-only and untracked since 2026-08-20. Do not re-add it to git.
### 2026-09-27 — v1.13.0 released: GHSA-hmr8 fixed, production pinned to releases, hosted service read-only
-
- - **Merged and verified:** #395, #387, #390 (first-time contributor @EastArctica, thanked), then **#417**: `deploy-prod.yml` runs only on a `v*` tag or a manual run from `main`, locked by `tests/security/test_deploy_triggers.py`. #422: the deploy spec uses `mcp-remote --header-file`; a `${VAR}` inside `--header` is not expanded by mcp-remote (the client fills it before launch, so the token still lands in the process list). #425: test-harness token isolation.
- - **GHSA-hmr8 fix**, built in the advisory's temporary private fork and merged through the advisory (`3f23ed0`): `core/tool_policy.py` + `ALLOWED_WRITE_TOOLS` (unset: HTTP read-only, stdio unchanged; set but empty = `none`; `all` excludes code exec; unknown names stop startup; a removed tool is gone from the registry). `send_conversation` always previews; `get_conversation_details` never marks read (`auto_mark_read` removed); `update_syllabus` checks any supplied token, binds mode and body, and previews the exact HTML; `download_course_file` annotated as a write. Codex security review, 3 rounds on thread `01a0e4b7-b289-75b2-8e1c-af3644331a47`: R1 1 P1 / 2 P2 / 1 P3, R2 1 P2, R3 clean.
- - **v1.13.0** tagged on `c3207d3` at 10:14 PM CDT; all five channels verified (Release + `.mcpb` attestation exit 0, PyPI 200, Registry `isLatest`, site deployed, prod `azure-gies-c3207d3`). The hosted instance's write policy is set per `internal/ops-hosted.local.md` and was confirmed live after the deploy. Advisory: patched version set to 1.13.0, still a draft.
- - **Gotcha:** the HTTP wiring tests passed in the fix worktree and CI but failed on merged `main`, because the primary checkout's `.env` sets `CANVAS_API_TOKEN` and the HTTP startup guard exits. Run the suite in the primary checkout on the merged commit before tagging.
- - **Filed:** #418 to #421 (raw dates, guarded edits, honest pagination incl. an `assign_peer_review` placeholder write past 100 submissions, anonymous discussions). Private follow-ups are tracked outside this repo (see `internal/session-history.md`). Closed #399; replied on #375 and #391. Removed merged worktrees and branches.
- - **Next:** (1) Vishal: publish GHSA-hmr8 and request a CVE (its description still carries the 09-24 "Proposed fix"); hosted-instance follow-ups are in `internal/session-history.md` and `internal/ops-hosted.local.md`. (2) Decide GHSA-7pp5 (triage since 07-04). (3) Fix the #420 placeholder-write bug. (4) `scripts/rotate-canvas-token.mjs` does not know `--header-file`.
-
- ## ⚠️ Adoption numbers: what is safe to publish (2026-08-21)
-
- Before quoting any adoption figure for this project in a paper, report, or institutional
- document:
-
- - **Never print a PyPI download count.** It swings by more than an order of magnitude month to
- month — 521 (March), 7,426 (2026-08-10), 2,208 (2026-08-17). Whatever you quote will be wrong
- within weeks and looks cherry-picked either way.
- - **Stars / forks / contributors are stable** and are the defensible numbers. Dated snapshot:
- **194 stars / 65 forks / 19 contributors (2026-08-17)**; 198 / 67 on 2026-08-20. **Re-pull from
- the GitHub API on the day the document is finalised** and state the date alongside.
- - **Two claims in circulation are NOT verified** and are flagged internally as author-promotion
- statements: *"over 18,000 clones"* and *"the University of Michigan selected it as the sole
- Canvas MCP candidate for campus deployment."* Do not repeat either without a primary source.
-
- **Institutional posture — be precise.** A private, Entra-gated hosted instance serves Gies course
- staff; the **public hosted server was retired**. The only Illinois review artifact is Adam King's
- LRA — Mark Reynolds declined to initiate a campus review on 2026-08-17. **Do not imply a campus
- security review or blessing that does not exist.**
+ - **Merged:** #395, #387, #390, #417 (production deploys only on a tag or manual run), #422 (`mcp-remote --header-file`), #425.
+ - **GHSA-hmr8 fix** (`3f23ed0`): `core/tool_policy.py` + `ALLOWED_WRITE_TOOLS`; `send_conversation` always previews; `get_conversation_details` never marks read. Codex security review clean at round 3.
+ - **v1.13.0** tagged on `c3207d3`; all five channels verified. Advisory patched version set to 1.13.0, still a draft.
+ - **Next:** publish GHSA-hmr8; decide GHSA-7pp5; fix the #420 placeholder write; `scripts/rotate-canvas-token.mjs` does not know `--header-file`.