# 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](/mcp/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](https://github.com/JohannsenLum/canvas-api-mcp/issues/3) · 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):

```python
# 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:

1. **Unhandled exception.** Every other failure

[Read the thread](https://github.com/JohannsenLum/canvas-api-mcp/issues/16) · 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

```bash
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](https://github.com/JohannsenLum/canvas-api-mcp/issues/2) · 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/45) · 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/42) · 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/41) · 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:

```python
    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](https://github.com/JohannsenLum/canvas-api-mcp/issues/40) · 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/35) · 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:

```python
# 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/50) · 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/48) · 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:

```python
# 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/44) · 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](https://blog.modelcontextprotocol.io/posts/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](https://github.com/JohannsenLum/canvas-api-mcp/issues/36) · 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/34) · 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/33) · 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/32) · 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/24) · 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](https://github.com/JohannsenLum/canvas-api-mcp/issues/23) · 2026-08-07 · closed · 1 comment

The remaining reports are on [the project's issue tracker](https://github.com/JohannsenLum/canvas-api-mcp/issues).
