Reported issues for Canvas API MCP
Pod holds 19 of 35 problems reported by people outside the maintainer team. Issues filed by the project's own owners, members and collaborators are excluded entirely — a maintainer's release checklist is not a warning to a prospective user.
Back to Canvas API MCP.
Most discussed
Test gap: no boundary test at LOW_QUOTA_THRESHOLD
Found by an adversarial review of the test suite.
client.py throttles when X-Rate-Limit-Remaining drops below LOW_QUOTA_THRESHOLD
(100). tests/test_throttle.py covers 42 (throttles) and 600 (doesn't), but nothing
at the boundary.
An off-by-one mutation — <= instead of < — would pass the whole suite.
Fix: add cases at exactly 100.0 (must NOT throttle) and 99.9 (must throttle).
Mutate the operator locally first and confirm your new test actually catches it.
Rate limiting is
Read the thread · 2026-08-07 · closed · 3 comments
do_read_file's raw file download has no error handling and no test for its no-Authorization-header invariant
What happens
do_read_file in src/canvas_api_mcp/tools/content.py fetches the pre-signed download URL with a fresh, bare httpx.AsyncClient (line 100-102):
# The download URL is pre-signed and must NOT carry the Authorization header.
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as raw:
file_response = await raw.get(download_url)
file_response.raise_for_status()
Two problems in this exact block:
- Unhandled exception. Every other failure
Read the thread · 2026-08-07 · closed · 2 comments
Add --test and --config CLI subcommands for debugging outside an MCP client
Problem
The server only speaks JSON-RPC over stdio, so when it doesn't work there is nothing a user can inspect. They get silence inside a client they can't debug, and no way to answer "is my token even valid?"
Proposal
canvas-api-mcp --test # call whoami, print the account name and course count
canvas-api-mcp --config # print resolved config with the token REDACTED
--test should exit non-zero on failure and print the translated error from
client.py (which alrea
Read the thread · 2026-08-07 · closed · 2 comments
docs/DESIGN.md is stale: no get_syllabus, no mention of fencing, and 21 documented tools against 19 registered
What is wrong
docs/DESIGN.md describes an older version of this server. Checked against the
current tree:
get_syllabus mentioned in DESIGN.md 0 times (tool exists, shipped in 1.0.0)
safety.py / fencing mentioned 0 times (the largest architectural
addition in the project)
tools documented in the table 21
tools actually registered 19
So the document is simultaneously missing things th
Read the thread · 2026-08-12 · closed · 1 comment
read_discussion, get_page, and 7 other read tools return instructor/classmate text unfenced, so course content can act as instructions to the model holding post_discussion_reply and submit_assignment
What happens
None of the 16 curated tools mark instructor- or classmate-authored text as untrusted before handing it back to the model. Every field below is copied straight out of the Canvas JSON response into the tool's return dict, unmodified: no delimiter, no provenance note, no length cap tied to a safety boundary, nothing that would stop the text from being read as an instruction rather than as data.
This server has an equivalent problem to the one solved in `johannsenlum/linkedin-api
Read the thread · 2026-08-10 · closed · 1 comment
read_discussion, course_announcements, get_assignment, get_page, get_syllabus, and read_file return instructor and classmate text unfenced, though 3 write tools can act on it
What is wrong
Every tool that returns text someone else wrote in Canvas (an instructor's
announcement, a classmate's discussion reply, a page or syllabus body, an
assignment's instructions, a grader's submission comment) hands that text
back exactly as Canvas sent it: no wrapping, no nonce, no "this is data, not
instructions" label. There is nothing in this codebase equivalent to
fence() / clean() / truncate() in the sibling project
[linkedin-api-mcp](https://github.com/JohannsenLum/
Read the thread · 2026-08-10 · closed · 1 comment
canvas_request returns Canvas content completely unfenced, letting any classmate or instructor's text act as instructions
What happens
do_request in src/canvas_api_mcp/tools/gateway.py:59-73 hands response.data straight back to the model, exactly as Canvas sent it:
try:
response = await client.request(verb, path, params=params, json=body)
except CanvasError as exc:
return {...}
return {
"data": response.data,
"truncated": response.truncated,
"pages_fetched": response.pages_fetched,
}
There is no clean/truncate/fence step of any kind
Read the thread · 2026-08-10 · closed · 1 comment
get_page cannot fetch a course syllabus, though its description and the README both claim it can
What's wrong
get_page's tool description says: "Get the content of a Canvas page in a course, such as a syllabus or a weekly overview" (src/canvas_api_mcp/tools/content.py, in its @mcp.tool registration). do_get_page implements this by calling GET courses/{course_id}/pages/{page_url}, the Canvas Pages (wiki) API.
A course's syllabus is not a wiki page in Canvas. It lives on the course object itself, as the syllabus_body field, retrieved with `GET /courses/:id?include[]=syllabus_
Read the thread · 2026-08-10 · closed · 1 comment
Most recent
--config omits CANVAS_TIMEOUT, and nothing stops the next variable going missing too
What is wrong
--config was added in #43 to print the resolved configuration. It reports three variables
but the server reads four:
# src/canvas_api_mcp/server.py
def _print_config(config: Config) -> None:
print(f"CANVAS_BASE_URL: {config.base_url}")
print(f"CANVAS_TOKEN: {_redact_token(config.token)}")
print(f"CANVAS_MAX_PAGES: {config.max_pages}")
# CANVAS_TIMEOUT is missing
Config carries base_url, token, max_pages and timeout. CANVAS_TIMEOUT l
Read the thread · 2026-08-15 · open · 0 comments
No structural test enforces which fields are fenced, so read_file went unfenced since 1.0.0
What is wrong
The linkedin-api-mcp sibling has tests/test_fencing_coverage.py, a structural test that
walks every tool module and fails if a prose-shaped field is returned without a fence. This
repo has no equivalent, and the consequence is already visible: read_file has been
returning unfenced document text since fencing landed in 1.0.0, and it was found by reading
code rather than by a failing test.
Every fencing test here names one field:
tests/test_safety.py test_get_page_fen
[Read the thread](https://github.com/JohannsenLum/canvas-api-mcp/issues/49) · 2026-08-15 · open · 0 comments
### read_file returns extracted document text unfenced, though get_page and get_syllabus fence theirs
## What is wrong
`read_file` extracts text from instructor-uploaded documents (PDF, PPTX, DOCX, plain text)
and returns it with no fencing at all, while its two neighbours in the same module fence
theirs.
`src/canvas_api_mcp/tools/content.py`:
```python
line 75: "body": guard(page.get("body"), BODY_LIMIT, "page.body")
line 91: "syllabus_body": guard(course.get("syllabus_body"), BODY_LIMIT, "syllabus.body")
"text": <extracted document text, returned raw>
Demo
Read the thread · 2026-08-15 · open · 0 comments
get_assignment reports partial failure as a 'note' string while whats_due uses a 'warnings' list
What is wrong
This server has two different shapes for "the call mostly worked, but part of it did not", and callers have to know which tool uses which.
do_whats_due collects a list:
# src/canvas_api_mcp/tools/student.py:172 and :212
"warnings": warnings,
backed by _safe_fetch, which catches CanvasError and httpx.HTTPError and
appends a formatted message to a caller-supplied warnings: list[str]. The
comment there explains why a list matters: whats_due merges thre
Read the thread · 2026-08-12 · closed · 0 comments
Server still negotiates MCP protocol 2025-11-25, blocked on FastMCP allowing mcp>=2
What's wrong
This server negotiates MCP protocol version 2025-11-25. The current specification is
2026-07-28, announced on 2026-07-28.
The cause is upstream, not in this repository. fastmcp currently pins mcp<2, and only
mcp>=2.0.0 speaks the new protocol version. Verified:
# this repo's environment
mcp 1.29.0 LATEST_PROTOCOL_VERSION = 2025-11-25
# clean venv
pip install mcp==2.0.0 -> LATEST_PROTOCOL_VERSION
[Read the thread](https://github.com/JohannsenLum/canvas-api-mcp/issues/39) · 2026-08-10 · open · 0 comments
### The 30 second HTTP timeout is hardcoded, with no CANVAS_TIMEOUT variable to raise or lower it
## What's wrong
`CanvasClient.__init__` constructs its `httpx.AsyncClient` with a fixed `timeout=30.0` (`src/canvas_api_mcp/client.py:164`):
```python
self._client = httpx.AsyncClient(
base_url=config.base_url,
headers={...},
timeout=30.0,
transport=transport,
follow_redirects=True,
)
There is no way to change this without editing source. Contrast this with CANVAS_MAX_PAGES, which is exactly this kind of tunable and already has a full env-var pattern in `src/canvas_a
Read the thread · 2026-08-10 · closed · 0 comments
list_files, list_assignments, and four other tools drop the pagination truncation flag, hiding incomplete results
What's wrong
CanvasClient.request already computes whether a paginated response was cut short. CanvasResponse (src/canvas_api_mcp/client.py:48) carries a truncated: bool field, and the pagination loop sets it to True when the next page's Link header points off-origin or when CANVAS_MAX_PAGES is reached (client.py:318, :321; default 10, each page up to 100 records, so a hard cap around 1,000 records per call).
Exactly one tool surfaces this. do_request in gateway.py retur
Read the thread · 2026-08-10 · open · 0 comments
No tool exposes Canvas quizzes, though whats_due's own description promises them
What's wrong
whats_due's tool description says it lists "what is due for the user across all courses (assignments, quizzes, and scheduled events)" (src/canvas_api_mcp/tools/student.py, around line 437). In practice whats_due can only ever surface a quiz as a bare due-date entry, because it just merges /users/self/todo, /users/self/upcoming_events, and /planner/items (student.py:125). It never touches the Quizzes API. There is no tool anywhere that fetches a quiz's own detail (i
Read the thread · 2026-08-10 · open · 0 comments
Eleven read-only tools raise CanvasError instead of the documented error dict, crashing the tool call
What's wrong
The house style is that tools never raise: a Canvas failure comes back as {"error": true, "status": ..., "message": ..., "hint": ...} so a calling model can check result.get("error"). Five tools already follow this: do_post_discussion_reply (src/canvas_api_mcp/tools/discussions.py:96), do_get_assignment and do_submit_assignment (src/canvas_api_mcp/tools/student.py:276, :416), do_read_file (src/canvas_api_mcp/tools/content.py:84), and do_request (`src/canvas
Read the thread · 2026-08-10 · open · 0 comments
read_file raises instead of returning a structured error when the file download fails
What happens
do_read_file in src/canvas_api_mcp/tools/content.py promises never to raise — every other failure path (metadata 404, missing download URL, unsupported file type) is caught and turned into the tool's {"error": True, "status": ..., "message": ...} contract, and tests/test_extract.py pins each of those down.
The raw file download is the one path that isn't covered. file_response.raise_for_status() around line 100 has no try/except, so a failed download escapes as a
Read the thread · 2026-08-07 · closed · 1 comment
read_discussion crashes with RecursionError on deeply nested reply chains
What happens
_flatten in src/canvas_api_mcp/tools/discussions.py:19 walks the reply tree with one stack frame per nesting level. A discussion where students keep replying to the latest reply (rather than to the root) builds a single deep chain — and once it passes Python's default recursion limit (~1000), _flatten raises RecursionError.
do_read_discussion doesn't catch it, so the whole read_discussion tool call fails hard for that topic. There's no graceful degradation, which is
Read the thread · 2026-08-07 · closed · 1 comment
The remaining reports are on the project's issue tracker.