What it does
Canvas is the learning-management system my university runs on: courses, assignments, due dates, syllabi, files. canvasmcp1 is an open-source Model Context Protocol2 server that lets an AI assistant read it. Instead of clicking through five course pages, I can ask “What’s due this week?” or “Summarize the syllabus for EECS 678.”
The original authenticates by reading Canvas session cookies out of Chrome on the same machine, which suits a laptop. I wanted it running around the clock on my home server: a headless Linux box with no Chrome profile to read.
What I changed
One focused commit to my fork,3 kept compatible with upstream behavior: Chrome stays the default, and everything new is opt-in.
- Firefox as a cookie source. With
CANVAS_COOKIE_BROWSER=firefox, the server reads thecanvas_sessionand_csrf_tokencookies for the configured Canvas host using browser_cookie3,4 prefers the most specific matching domain, and ignores expired cookies. - An explicit override.
CANVAS_SESSION_COOKIEandCANVAS_CSRF_TOKENcan be supplied directly, and setting one without the other is an error rather than a silent half-login. The README tells users to treat them as credentials. - Files straight into the chat. A new
read_course_filetool returns a Canvas file as an MCP embedded binary resource, so a compatible client can read a PDF inline instead of downloading it to disk first. - Tests. About 270 lines of new tests cover the auth paths and inline file reads.
A size limit that holds
Inlining files into a chat needs a ceiling. The limit defaults to 25 MB and is capped at 50 MB, and it’s enforced twice: once against Canvas’s file metadata, and again while reading, because metadata can be missing or wrong. Reading at most one byte past the limit keeps memory bounded either way, and anything larger points the user to the existing download tool.
def read_course_file(course_id, file_id, max_size_mb=25): max_size_mb = max(1, min(int(max_size_mb), 50)) max_bytes = max_size_mb * 1024 * 1024 info = client.get_file(course_id=course_id, file_id=file_id) if isinstance(info.get("size"), int) and info["size"] > max_bytes: raise ValueError("... Use download_course_file instead.") ... # Bound memory even when Canvas metadata is missing or inaccurate. data = downloaded_file.read(max_bytes + 1) if len(data) > max_bytes: raise ValueError("... Use download_course_file instead.") return ToolResult(content=[..., EmbeddedResource(...)])
read_course_file, condensed from the fork.Running it on a server
The fork reads cookies from a browser session, so the server needs a browser someone has logged into. My Docker image builds the fork pinned to a specific commit and runs Firefox ESR on a virtual display, reachable only through noVNC5 over an SSH tunnel. I log into Canvas there once. The MCP server reads that profile’s cookies, and a keepalive runs every 20 minutes to keep the session fresh. No secrets are baked into the image: the browser profile and all state live in a mounted data volume.
One-time login
Docker on my server
Assistants
Limits
Session cookies are credentials, and anyone with them is logged in as me, which is why the browser profile never leaves the server’s data volume. Inline display depends on the MCP client: an embedded resource is not guaranteed to appear as a native chat attachment. And upstream’s scheduled-submission feature relies on macOS launchd, so it doesn’t work on the Linux server; I haven’t needed it.
References
- ynbh/canvasmcp: MCP server for Canvas.
- Anthropic. Model Context Protocol.
- Blake192/canvasmcp: my fork, with Firefox cookie auth and inline MCP file resources.
- browser_cookie3: load browser cookies into Python.
- noVNC: an HTML VNC client.