canvas-mcp-server
Provides read-only access to Canvas LMS data, including courses, assignments, grades, submissions, announcements, discussions, modules, pages, and files via the Canvas REST API.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@canvas-mcp-serverWhat assignments are due this week?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
canvas-mcp-server
MCP server for the Canvas LMS REST API. Gives an LLM read access to your courses, assignments, grades, submissions, announcements, discussions, modules, pages and files.
20 tools, all read-only.
Requirements
Node.js 18+
A Canvas account at any institution
An access token from Account → Settings → New Access Token in your Canvas web UI
Install
npm install
npm run buildConfigure
Canvas has no shared API host — every institution runs its own. Both variables below are required.
{
"mcpServers": {
"canvas": {
"command": "node",
"args": ["/absolute/path/to/canvas-mcp-server/dist/index.js"],
"env": {
"CANVAS_BASE_URL": "https://bcourses.berkeley.edu",
"CANVAS_ACCESS_TOKEN": "your-token-here"
}
}
}
}Variable | Required | Default | Purpose |
| yes | — | Your institution's Canvas host, scheme included, no trailing path |
| yes | — | Account → Settings → New Access Token |
| no |
| Per-request timeout |
| no |
|
|
| no |
| HTTP transport bind address |
| when hosted | — | Serves the endpoint at |
| no | localhost + claude.ai | Comma-separated origin allowlist |
Inspect the tools interactively:
CANVAS_BASE_URL=https://your.canvas CANVAS_ACCESS_TOKEN=your-token npm run inspectDeploying (for Claude mobile / claude.ai connectors)
Claude connects to custom connectors from Anthropic's cloud, not from your device, so mobile and claude.ai need this reachable over public HTTPS. Claude Code and Claude Desktop don't — use stdio there instead.
1. Generate a path secret
openssl rand -hex 32The server refuses to start on a non-loopback interface without MCP_PATH_SECRET set, because a public endpoint holding your Canvas token is an open proxy to your account. With it set, the endpoint moves to /mcp/<secret> and every other path returns 404 — including a wrong secret, so probing the host doesn't reveal that an MCP server lives there.
2. Deploy
The included Dockerfile and railway.json work as-is on Railway, Render, or Fly. The image sets TRANSPORT=http and HOST=0.0.0.0 and runs as a non-root user. Set three variables in the platform's dashboard:
Variable | Value |
| your institution's Canvas host |
| your token |
| the value from step 1 |
PORT is injected by the platform. /healthz is an unauthenticated liveness probe.
3. Verify
curl -s https://your-app.up.railway.app/healthz4. Add the connector
On claude.ai in a browser — connectors can't be added from the mobile app:
Customize → Connectors → Add custom connector
URL:
https://your-app.up.railway.app/mcp/<secret>On your phone, open a chat and enable it under + → Connectors
Treat that URL like a password. If it leaks, rotate MCP_PATH_SECRET and re-add the connector.
Tools
Courses — canvas_list_courses, canvas_get_course, canvas_get_grades, canvas_list_enrollments, canvas_get_profile
Assignments — canvas_list_assignments, canvas_get_assignment, canvas_get_submission, canvas_list_quizzes
Planner — canvas_list_planner_items, canvas_list_upcoming, canvas_list_calendar_events
Announcements and discussions — canvas_list_announcements, canvas_list_discussions, canvas_get_discussion
Course content — canvas_list_modules, canvas_list_module_items, canvas_list_pages, canvas_get_page, canvas_list_files
Every read tool takes response_format: "markdown" | "json". Markdown is the default and is optimized for an LLM reading it; JSON is the full structured payload. structuredContent is always populated regardless of format.
Examples
"What's due this week?"
→ canvas_list_planner_items with end_date a week out. Spans every course in one call and reports submission state. It starts from today by default, so for "what am I behind on" pass an explicit earlier start_date.
"What are my grades?"
→ canvas_get_grades. One call, every active course, current score and letter grade.
"What did my professors announce this week?"
→ canvas_list_courses for ids, then canvas_list_announcements with all of them at once.
"What do I actually have to do for project 2?"
→ canvas_list_assignments with search_term="project 2" to get the id, then canvas_get_assignment for the full instructions.
Design notes
Read-only by construction. Every tool carries readOnlyHint: true and destructiveHint: false, and the client has no write path exposed. Canvas tokens carry the full authority of your account — they can submit assignments, post to discussions, and change profile settings — so the server deliberately declines to expose any of that. A test asserts this: if a write tool is ever added, the suite fails.
The base URL is required, not defaulted. Unlike single-tenant APIs, Canvas runs one instance per institution. There is no sensible default, and a token issued by one school's Canvas is meaningless at another, so the server fails at startup rather than misleading you with 401s later.
Pagination lives in a header. Canvas reports "is there a next page" in an RFC 5988 Link header and never returns a total count. Those URLs are documented as opaque, so has_more is read from the header while page/per_page stay the caller-facing controls — an agent gets a simple next_page to follow instead of a cursor to thread.
Ids are requested as strings. Canvas ids are 64-bit integers, which JavaScript cannot represent exactly. The client sends Accept: application/json+canvas-string-ids, which Canvas honours by returning every id as a string, so ids survive a JSON round trip intact.
HTML is flattened before it reaches the model. Assignment descriptions, announcements, discussion posts and pages are all stored as HTML. Passing that through verbatim burns enormous context on markup, so tags become line breaks, entities are decoded, and long bodies are excerpted with the html_url kept for the full version.
include[] is not exposed. Canvas has two dozen include options, they differ between the list and single-course endpoints, and most control fields an agent has no use for. Each tool requests what it needs and surfaces only the toggles that change what a user would see — include_syllabus, include_grades, include_submission.
Course ids are normalised into context codes. Some Canvas endpoints address courses as course_1234 rather than 1234. Both forms are accepted everywhere and converted, so the agent never has to remember which endpoint wants which.
Errors resolve to next actions. A 404 names the tool that produces valid ids for that resource. A 403 distinguishes a permissions problem from an exhausted rate limit, which Canvas confusingly returns under the same status. A 401 points out that a token from one school's Canvas will not work at another.
Two Canvas quirks are handled rather than passed on. The grade a course reports under enrollments[].computed_current_score is the same number the Enrollments API calls grades.current_score; both are read. And a planner item's submissions field is the boolean false — not an object — when nothing is submittable, which is checked before it is read.
Caveats
Announcements cannot be listed globally: Canvas requires at least one course id, so
canvas_list_courseshas to run first.canvas_list_discussionsapplies itsscopefilter after paginating, so a filtered page can come back shorter thanper_pagewithout being the end of the results.Canvas omits module items from the list response for modules it considers large;
canvas_list_module_itemsfetches them.Pages are addressed by url slug (
week-1-reading), not title.canvas_list_pagesreturns the slug in itsurlfield.The calendar endpoint accepts at most 10 courses and silently ignores the rest;
canvas_list_calendar_eventsreports when it trims.Grades reflect only what an instructor has posted, and are omitted entirely for courses configured to hide final grades.
Project layout
src/
├── index.ts # entry point, transport selection
├── constants.ts # enum values, limits, character limit
├── types.ts # interfaces for every Canvas entity
├── services/
│ └── canvas-client.ts # fetch wrapper, auth, Link pagination, error → guidance mapping
├── schemas/
│ ├── inputs.ts # Zod input schemas
│ └── outputs.ts # structuredContent schemas
├── formatters/
│ ├── response.ts # pagination, truncation, HTML flattening, format dispatch
│ └── entities.ts # per-entity markdown rendering
└── tools/
├── courses.ts
├── assignments.ts
├── planner.ts
├── announcements.ts
└── content.tsTests
npm run build
npm test # 43 checks: MCP handshake, tools, pagination, formatting, errors (mocked API)
npm run test:http # 19 checks: config validation, path-secret gating, method handling, originsBoth suites run against a local mock, so no token or network access is needed.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Gateway between LLM agents and world data through eight tools and a bundled endpoint catalog.
Read-only MCP server for ClassQuill, a tutoring-business-management platform.
Read-only MCP server for Muovi, Argentina's trust-first local services marketplace (6 tools).
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/RyK57/canvas-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server