Skip to main content
Glama

canvas-mcp

No more time wasted checking modules, announcements and assignments across your courses and doubting that you've missed something. Automate everything with Claude.

One question pulls every assignment, grade, and announcement across all your courses, instantly, in a single answer:

Claude answering "When are my midterms?" with a table of five exam dates pulled from Canvas across ENPH 259, MATH 215 and MATH 217, including one the instructor said may move

Not just a dump of due dates — it reads the announcements too, so a midterm the instructor only mentioned in passing shows up, and it says plainly which courses posted nothing rather than leaving you to assume there's nothing there.

Claude answering "What are my assignments for next week?" with five unsubmitted items across four courses, the already-finished ones listed separately, and a note about what falls due right afterwards

It knows what you have already submitted, so "what do I still have to do" is a question it can actually answer.

Use with other MCPs like Google Calendar to automate adding all your assignments and deadlines to one place.

No API setup. No access token, no developer key, no admin approval, no account to make — the things that stop most Canvas tools dead at a school that locks them down. It borrows the login your browser already has.

One file, one paste. It installs into Claude Desktop as a single file. No Node, no terminal, no config files.

Read-only by construction: it can look at your Canvas, never change it. It cannot submit, post, or delete anything. Details below.

Install it

You need Claude Desktop and a Canvas login. Nothing else.

1. Download the extension

⬇ Download canvas-ubc.mcpb — grab the .mcpb file from the latest release (~4 MB).

Your browser may warn that it's an unusual file type. Keep it.

2. Open it

Double-click the file, or drag it onto the Claude Desktop window. Claude Desktop shows an install dialog that lists the eight tools it's adding, before you agree to anything.

If double-clicking does nothing, open Claude Desktop → Settings → Extensions and use the option there to install an extension from a file.

This is the only setup step, and the only fiddly one. It's how the extension proves it's you — it borrows the login your browser already has.

  1. Log into canvas.ubc.ca in Chrome.

  2. Press F12. A panel opens; click the Application tab at the top of it.

  3. In that panel's left sidebar: Storage → Cookies → https://canvas.ubc.ca.

  4. Find the row named canvas_session. Click it, then copy the whole Value — a long scramble of letters and numbers. Not the name, not the row.

  5. Paste it into the Canvas session cookie field on the install screen and save.

Two notes:

  • It has to be the Application tab. The cookie is httpOnly, so the Console tab genuinely cannot see it. If you got a short value or nothing at all, you copied the wrong thing.

  • Not at UBC? Change Canvas address to your school's Canvas, and use that domain in step 3. If your school's cookie isn't called canvas_session (some call it _normandy_session), the extension can't reach it — you'd need to run from source instead.

4. Add your calendar feed (optional, 30 seconds, worth it)

In Canvas, open Calendar and click Calendar Feed at the bottom right. Copy the whole URL into Calendar feed URL.

That URL needs no login, so deadline questions keep answering during the hours between your session expiring and you noticing. How it degrades.

5. Ask Claude

catch me up on school

Others worth trying:

when are my midterms? what are my assignments for next week? did any of my professors post anything today? how am I doing in my courses? what exactly does the CPEN 221 lab want?

Both settings fields are marked sensitive, so Claude Desktop keeps them in the macOS Keychain or Windows Credential Manager rather than in a file on your disk.

Related MCP server: Canvas LMS MCP Server

When Claude says your session expired

Canvas sessions last about a day, so this will happen roughly daily. The fix is 20 seconds:

  1. Copy a fresh cookie — the same steps as above.

  2. Settings → Extensions → Canvas (UBC), replace Canvas session cookie, save.

The extension restarts itself. Nothing else to do, and every tool tells you this when it happens, so you don't have to remember it.

Why can't it just stay logged in? UBC logs you in through CWL single sign-on, and Canvas never offers its own "Stay signed in" box under SSO. So there's a ~1-day cookie and no way to extend it. That's your school's login, not something this extension can fix — which is why the calendar feed in step 4 exists.

If something looks wrong

Ask Claude to check the Canvas connection first (that runs check_canvas_auth), then:

What you see

What's going on

"Session expired" on everything

Normal daily expiry. Repaste the cookie.

Still expired right after repasting

The value went stale between copying and pasting, or you copied the cookie's name instead of its Value. Load Canvas in a fresh tab and copy again.

Deadlines answer but grades and announcements don't, with a warning banner

Your cookie is dead and you're being served from the calendar feed. Repaste the cookie.

Claude doesn't seem to have the tools at all

Check Settings → Extensions — the extension may be disabled, or Claude Desktop may need a restart.

403 errors that aren't about rate limits

Your school has a bot filter in front of Canvas that the extension can't get past on its own. This needs the source setup.

What it can do

Ask about

Tool

"Catch me up on school." Due soon + overdue + new announcements + newly graded, in one answer. Best starting point.

get_daily_briefing

"What do I have due?" Assignments, quizzes, discussions, events and to-dos, grouped by day, each marked submitted / not submitted / graded.

get_upcoming_work

"What did my professors post?" Recent announcements with body text.

get_announcements

"How am I doing?" Current grade per course, plus what was graded recently.

get_grades

One course in depth: syllabus, module progress, instructor contacts.

get_course_detail

One assignment in depth: full instructions, rubric, due date, your submission status.

get_assignment_detail

Your active courses and their ids.

list_courses

"Is the connection alive?"

check_canvas_auth

What it can and cannot do

  • It can only read. The one piece of code that talks to Canvas hardcodes method: 'GET' and exposes no way to send anything else — there is no post/put/delete to reach for by accident. It sees exactly what you see when you log in, and it cannot submit an assignment, reply to a discussion, or change a setting.

  • Your cookie stays on your machine. It goes from your browser into Claude Desktop's OS credential store and from there to your school's Canvas. Nowhere else.

  • Don't share the cookie with anyone, including in a screenshot. For as long as it's alive it is your Canvas login.

  • Reading your own coursework is data you're already authorized to see, but automated access may still fall under your school's acceptable-use policy. This is built to be a polite client — read-only, cached, low request volume. Keep it that way.

The calendar-feed fallback

The session cookie is the fragile part, so there's a backstop. Canvas gives every user a personal .ics calendar feed (Calendar → Calendar Feed) whose URL needs no cookie and no admin — the code in the URL is itself the credential. With it configured, get_upcoming_work and get_daily_briefing keep serving deadlines after the cookie dies, behind an explicit degraded-mode banner. Announcements and grades genuinely can't work that way, and the banner says so rather than letting a thin answer look complete.


Developing

Everything below is for working on the extension, not for using it.

Build the bundle

npm install
npm run pack        # -> canvas-ubc.mcpb, ~4 MB

That's the whole release process: hand the resulting file to a classmate, or publish it. scripts/pack.mjs:

  1. Builds, then asks the compiled server for its real tools/list and fails if manifest.json disagrees — the install screen shows that list, and a stale entry there is a promise the server does not keep.

  2. Stages dist-mcpb/ from an explicit allowlist: build/src, manifest.json, icon.png, a trimmed package.json (Node needs "type": "module" next to the ESM output), and the production dependency tree resolved via npm ls --omit=dev. build/scripts is excluded — the cookie helper is a maintainer tool.

  3. Walks the staged tree and refuses to pack if anything .env-, .pem-, or .key-shaped is in it.

  4. Runs mcpb validate, then mcpb pack.

Step 3 is the point of staging at all. .env in this repo holds a live Canvas session cookie for whoever runs the build; a bundle built by zipping the working directory would hand a classmate your account. An allowlist cannot leak a file nobody remembered to deny. .mcpbignore covers the same ground for anyone who runs mcpb pack here by hand.

The icon is generated too — npm run icon redraws the 512×512 icon.png from scripts/make-icon.mjs, so it can be recoloured without a design tool.

Publish a release

The download link at the top of this README points at the repo's latest GitHub Release, so publishing is a tag:

# bump "version" in package.json AND manifest.json first — they must match the tag
npm version 0.2.1 --no-git-tag-version   # package.json + lockfile; edit manifest.json to match
git tag v0.2.1 && git push origin v0.2.1

.github/workflows/release.yml builds the bundle on a clean checkout, refuses to continue if the tag and the two version fields disagree, and attaches canvas-ubc.mcpb to the release.

Or publish without a terminal: Actions → Release → Run workflow, and type the tag (v0.2.1) into the box. gh release create --target creates the tag itself, so that path needs nothing but the repo — useful when a local git push --tags is blocked. Leave the box empty and it just builds the bundle as a downloadable artifact, which is how to check a build without publishing anything.

Building on a runner is also the safer default: npm run pack guards against shipping credentials, but a runner has no .env to leak in the first place.

Run it from source

The development loop, and the escape hatch for anything the extension's three settings fields can't express (a differently-named session cookie, a Cloudflare cf_clearance, a personal access token).

npm install
cp .env.example .env     # then fill it in
npm run build

Variable

Required?

What it is

CANVAS_BASE_URL

yes

Your Canvas origin, https, e.g. https://canvas.ubc.ca

CANVAS_SESSION_COOKIE

yes

The cookie value, copied as in step 3

CANVAS_SESSION_COOKIE_NAME

if not canvas_session

The cookie's name on your install. Upstream Canvas ships _normandy_session; installs rename it. Canvas silently ignores a name it doesn't recognize, so a mismatch looks exactly like an expired session — check_canvas_auth prints the name in use

CANVAS_ICS_FEED_URL

recommended

Canvas → Calendar → Calendar Feed. Keeps deadlines working when the cookie dies

CANVAS_REMEMBER_COOKIE

optional

A pseudonym_credentials cookie, if your Canvas sets one. Stretches refreshes to ~2 weeks. Does not exist under SSO

CANVAS_EXTRA_COOKIES

rarely

Raw name=value; name2=value2, e.g. cf_clearance=… behind Cloudflare. That one is tied to your IP and User-Agent, so it needs recopying more often than the session cookie

CANVAS_USER_AGENT

rarely

Overrides the default Chrome User-Agent

CANVAS_ACCESS_TOKEN

optional

A Canvas personal access token, if your school enables them. Takes priority over cookies and removes the expiry problem entirely

.env is gitignored. Keep it that way — it holds a live credential for your Canvas account.

Verify it works

npm run smoke

Talks to Canvas directly with no MCP layer involved, and prints your name, your courses, and your next few deadlines. Run this first whenever something is wrong — a bad cookie or a school that blocks session-authenticated API calls fails here in seconds with a clear reason, instead of showing up as a mysteriously empty tool result.

Connect it to Claude Code

The repo ships a .mcp.json, so Claude Code picks the server up when you open this folder. Two things to know:

  • Build before you connect. .mcp.json runs build/src/index.js, which doesn't exist until npm run build.

  • The server exits at startup if .env has no credentials, rather than serving tools that can only fail. So in a fresh clone the order matters: fill in .env, build, then start or reconnect Claude Code. If you were already running: /mcp → canvas → Reconnect.

Confirm with check_canvas_auth. To poke at the tools without Claude: npm run inspect (MCP Inspector).

Point Claude Desktop at the checkout

{
  "mcpServers": {
    "canvas": {
      "command": "node",
      "args": [
        "--env-file-if-exists=C:\\path\\to\\Canvas MCP\\.env",
        "C:\\path\\to\\Canvas MCP\\build\\src\\index.js"
      ]
    }
  }
}

In claude_desktop_config.json. This is for testing changes against Desktop; for anyone not editing the code, the bundle needs no paths, no Node, and no .env.

npm run cookie              # reads the value straight from your clipboard
npm run cookie -- <value>   # or pass it explicitly

Rewrites just the one line in .env (comments and your other settings survive), then verifies the cookie against Canvas immediately, so you find out it worked here rather than from a failing tool call later.

Then make the client pick it up:

  • If the canvas server is still connected, you're done — it re-reads .env on the next authentication failure and retries once with the new credential. (Only when the value actually changed, so a genuinely dead cookie still fails fast instead of looping.)

  • If the server is disconnected or its tools are missing, its process is gone and no amount of .env editing will reach it. Reload the tools: /mcp → canvas → Reconnect.

.claude/skills/canvas-cookie/SKILL.md is a project skill that walks Claude through that whole sequence, ending with check_canvas_auth — the step that's easy to skip by hand, since a refreshed .env does nothing for a server process that already exited.

Canvas's normal API auth is a personal access token, but many schools disable self-service token generation for students, and OAuth2 developer keys need an admin too.

Canvas's own web UI calls /api/v1 using your ordinary session cookie, and the server accepts that as a first-class auth path — load_user in lib/authentication_methods.rb tries token auth first and then falls back to PseudonymSession.find_with_validation. So this server sends the same cookie your browser does.

Two consequences worth knowing:

  • Read-only is enforced structurally. CanvasClient exposes one request primitive that hardcodes method: 'GET', and no caller can pass a method through. This also means no CSRF token is ever needed, since Rails only verifies CSRF on non-GET requests.

  • The cookie has to be re-pasted periodically — about daily under SSO, or every two weeks with a pseudonym_credentials cookie. When it lapses, every tool returns step-by-step refresh instructions rather than an error, and deadline tools fall back to the calendar feed.

What differs inside the bundle

The extension gets its settings from manifest.json's user_config, which Claude Desktop renders as a form and injects as environment variables. Two consequences the code accounts for:

  • There is no .env and no terminal, so every "here is how to fix this" message forks on isBundleInstall() (set by CANVAS_INSTALL=mcpb in the manifest) and tells extension users to edit the settings field instead. Telling someone to edit a file they cannot see is the worst thing these messages could do. The hot-reload in readEnvFile simply finds nothing and the server fails fast, which is correct: Claude Desktop restarts it on a settings change anyway.

  • A blank optional field can arrive as the literal string ${user_config.ics_feed_url}, so clean() in config.ts reads an unsubstituted placeholder as empty. Without that, an untouched calendar-feed box becomes a garbage URL that fails deep inside a request.

CANVAS_ACCESS_TOKEN, CANVAS_REMEMBER_COOKIE, CANVAS_EXTRA_COOKIES and CANVAS_SESSION_COOKIE_NAME are deliberately not in user_config: none apply at UBC, and every extra field in that form is one more thing a student has to decide about. They still work from a source checkout, which is what the troubleshooting table points at for the rare install that needs them.

Layout

src/
  index.ts              stdio entrypoint
  config.ts             env parsing and validation
  format.ts             shared text formatting (relative dates, scores, grouping)
  canvas/
    client.ts           the only module that talks to Canvas: GET-only, paginated,
                        retrying, cookie-rotating, with typed auth errors
    queries.ts          shared reads, cached so the briefing composes cheaply
    types.ts            Canvas object shapes
    html.ts             HTML → markdown (Canvas returns rich text everywhere)
    ics.ts              minimal iCalendar parser for the fallback
  tools/                one module per tool group, all read-only
scripts/
  refresh-cookie.ts     one-command cookie refresh, verified against Canvas
  smoke.ts              direct connectivity check, no MCP
  pack.mjs              builds and validates the .mcpb bundle
  make-icon.mjs         redraws icon.png
manifest.json           the extension: settings form, tool list, entry point
.github/workflows/
  release.yml           tag -> bundle -> GitHub Release
.claude/skills/
  canvas-cookie/        project skill: refresh the cookie and reload the tools
docs/                   README screenshots

Notes

  • Built on @modelcontextprotocol/server v2 (serveStdio), not the older v1 @modelcontextprotocol/sdk monolith — most examples online still show v1.

  • Requires Node ≥ 20; no dotenv, Node loads .env itself.

  • Tools return compact formatted text rather than raw JSON. That's the main lever on both token cost and answer quality.

Not included (deliberately)

Writing anything to Canvas, OAuth, remote hosting as a claude.ai connector, multi-user support.

Related MCP Connectors

Related MCP Servers