Canvas MCP Server
by DMontgomery40
Verified
- src
#!/usr/bin/env node
// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
Tool
} from "@modelcontextprotocol/sdk/types.js";
import { CanvasClient } from "./client.js";
import * as dotenv from "dotenv";
import {
CreateCourseArgs,
UpdateCourseArgs,
CreateAssignmentArgs,
UpdateAssignmentArgs,
SubmitGradeArgs,
EnrollUserArgs,
CanvasCourse,
CanvasAssignmentSubmission
} from "./types.js";
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
// Define the tools
const TOOLS: Tool[] = [
{
name: "canvas_create_course",
description: "Create a new course in Canvas",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Name of the course" },
course_code: { type: "string", description: "Course code (e.g., CS101)" },
start_at: { type: "string", description: "Course start date (ISO format)" },
end_at: { type: "string", description: "Course end date (ISO format)" },
license: { type: "string" },
is_public: { type: "boolean" }
},
required: ["name"]
}
},
{
name: "canvas_update_course",
description: "Update an existing course in Canvas",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course to update" },
name: { type: "string", description: "New name for the course" },
course_code: { type: "string", description: "New course code" },
start_at: { type: "string", description: "New start date (ISO format)" },
end_at: { type: "string", description: "New end date (ISO format)" },
license: { type: "string" },
is_public: { type: "boolean" }
},
required: ["course_id"]
}
},
{
name: "canvas_create_assignment",
description: "Create a new assignment in a Canvas course",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" },
name: { type: "string", description: "Name of the assignment" },
description: { type: "string", description: "Assignment description/instructions" },
due_at: { type: "string", description: "Due date (ISO format)" },
points_possible: { type: "number", description: "Maximum points possible" },
submission_types: {
type: "array",
items: { type: "string" },
description: "Allowed submission types"
},
allowed_extensions: {
type: "array",
items: { type: "string" },
description: "Allowed file extensions for submissions"
}
},
required: ["course_id", "name"]
}
},
{
name: "canvas_update_assignment",
description: "Update an existing assignment",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" },
assignment_id: { type: "number", description: "ID of the assignment to update" },
name: { type: "string", description: "New name for the assignment" },
description: { type: "string", description: "New assignment description" },
due_at: { type: "string", description: "New due date (ISO format)" },
points_possible: { type: "number", description: "New maximum points" }
},
required: ["course_id", "assignment_id"]
}
},
{
name: "canvas_submit_grade",
description: "Submit a grade for a student's assignment",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" },
assignment_id: { type: "number", description: "ID of the assignment" },
user_id: { type: "number", description: "ID of the student" },
grade: {
oneOf: [
{ type: "number" },
{ type: "string" }
],
description: "Grade to submit (number or letter grade)"
},
comment: { type: "string", description: "Optional comment on the submission" }
},
required: ["course_id", "assignment_id", "user_id", "grade"]
}
},
{
name: "canvas_enroll_user",
description: "Enroll a user in a course",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" },
user_id: { type: "number", description: "ID of the user to enroll" },
role: {
type: "string",
description: "Role for the enrollment (StudentEnrollment, TeacherEnrollment, etc.)"
},
enrollment_state: {
type: "string",
description: "State of the enrollment (active, invited, etc.)"
}
},
required: ["course_id", "user_id"]
}
},
{
name: "canvas_submit_assignment",
description: "Submit an assignment in Canvas",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" },
assignment_id: { type: "number", description: "ID of the assignment" },
user_id: { type: "number", description: "ID of the student" },
submission_type: { type: "string", description: "Type of submission (e.g., online_upload)" },
body: { type: "string", description: "Submission body or file URL" }
},
required: [
"course_id",
"assignment_id",
"user_id",
"submission_type"
]
}
},
{
name: "canvas_list_quizzes",
description: "List all quizzes in a course",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" }
},
required: ["course_id"]
}
},
{
name: "canvas_get_quiz",
description: "Get details of a specific quiz",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" },
quiz_id: { type: "number", description: "ID of the quiz" }
},
required: ["course_id", "quiz_id"]
}
},
{
name: "canvas_create_quiz",
description: "Create a new quiz in a course",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" },
title: { type: "string", description: "Title of the quiz" },
quiz_type: { type: "string", description: "Type of the quiz (e.g., graded)" },
time_limit: { type: "number", description: "Time limit in minutes" },
published: { type: "boolean", description: "Is the quiz published" },
description: { type: "string", description: "Description of the quiz" },
due_at: { type: "string", description: "Due date (ISO format)" }
},
required: ["course_id", "title"]
}
},
{
name: "canvas_update_quiz",
description: "Update an existing quiz",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" },
quiz_id: { type: "number", description: "ID of the quiz to update" },
title: { type: "string", description: "New title of the quiz" },
quiz_type: { type: "string", description: "New type of the quiz" },
time_limit: { type: "number", description: "New time limit in minutes" },
published: { type: "boolean", description: "Is the quiz published" },
description: { type: "string", description: "New description of the quiz" },
due_at: { type: "string", description: "New due date (ISO format)" }
},
required: ["course_id", "quiz_id"]
}
},
{
name: "canvas_delete_quiz",
description: "Delete a quiz from a course",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" },
quiz_id: { type: "number", description: "ID of the quiz to delete" }
},
required: ["course_id", "quiz_id"]
}
},
{
name: "canvas_list_modules",
description: "List all modules in a course",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" }
},
required: ["course_id"]
}
},
{
name: "canvas_get_module",
description: "Get details of a specific module",
inputSchema: {
type: "object",
properties: {
course_id: { type: "number", description: "ID of the course" },
module_id: { type: "number", description: "ID of the module" }
},
required: ["course_id", "module_id"]
}
},
{
name: "canvas_list_module_items",
description: "List all items in a module",
inputSchema: {
type: "object",
properties: {
course_id: { type: "string", description: "ID of the course" },
module_id: { type: "number", description: "ID of the module" }
},
required: ["course_id", "module_id"]
}
},
{
name: "canvas_get_module_item",
description: "Get details of a specific module item",
inputSchema: {
type: "object",
properties: {
course_id: { type: "string", description: "ID of the course" },
module_id: { type: "number", description: "ID of the module" },
item_id: { type: "number", description: "ID of the module item" }
},
required: ["course_id", "module_id", "item_id"]
}
},
{
name: "canvas_list_discussion_topics",
description: "List all discussion topics in a course",
inputSchema: {
type: "object",
properties: {
course_id: { type: "string", description: "ID of the course" }
},
required: ["course_id"]
}
},
{
name: "canvas_get_discussion_topic",
description: "Get details of a specific discussion topic",
inputSchema: {
type: "object",
properties: {
course_id: { type: "string", description: "ID of the course" },
topic_id: { type: "number", description: "ID of the discussion topic" }
},
required: ["course_id", "topic_id"]
}
},
{
name: "canvas_list_announcements",
description: "List all announcements in a course",
inputSchema: {
type: "object",
properties: {
course_id: { type: "string", description: "ID or URL of the course" }
},
required: ["course_id"]
}
}
];
class CanvasMCPServer {
private server: Server;
private client: CanvasClient;
constructor(token: string, domain: string) {
this.client = new CanvasClient(token, domain);
this.server = new Server(
{
name: "canvas-mcp-server",
version: "1.0.0"
},
{
capabilities: {
resources: {},
tools: {}
}
}
);
this.setupHandlers();
this.setupErrorHandling();
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[Canvas MCP Error]", error);
};
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupHandlers(): void {
// List available resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const courses = await this.client.listCourses();
return {
resources: [
{
uri: "courses://list",
name: "All Courses",
description: "List of all available Canvas courses",
mimeType: "application/json"
},
...courses.map((course: CanvasCourse) => ({
uri: `course://${course.id}`,
name: `Course: ${course.name}`,
description: `${course.course_code} - ${course.name}`,
mimeType: "application/json"
})),
...courses.map((course: CanvasCourse) => ({
uri: `assignments://${course.id}`,
name: `Assignments: ${course.name}`,
description: `Assignments for ${course.name}`,
mimeType: "application/json"
})),
...courses.map((course: CanvasCourse) => ({
uri: `users://${course.id}`,
name: `Users: ${course.name}`,
description: `Enrolled users in ${course.name}`,
mimeType: "application/json"
})),
...courses.map((course: CanvasCourse) => ({
uri: `grades://${course.id}`,
name: `Grades: ${course.name}`,
description: `Grade data for ${course.name}`,
mimeType: "application/json"
})),
...courses.map((course: CanvasCourse) => ({
uri: `quizzes://${course.id}`,
name: `Quizzes: ${course.name}`,
description: `Quizzes for ${course.name}`,
mimeType: "application/json"
})),
...courses.map((course: CanvasCourse) => ({
uri: `modules://${course.id}`,
name: `Modules: ${course.name}`,
description: `Modules for ${course.name}`,
mimeType: "application/json"
})),
...courses.map((course: CanvasCourse) => ({
uri: `discussion_topics://${course.id}`,
name: `Discussion Topics: ${course.name}`,
description: `Discussion topics for ${course.name}`,
mimeType: "application/json"
})),
...courses.map((course: CanvasCourse) => ({
uri: `announcements://${course.id}`,
name: `Announcements: ${course.name}`,
description: `Announcements for ${course.name}`,
mimeType: "application/json"
}))
]
};
});
// Read resource content
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const [type, id] = uri.split("://");
try {
let content;
switch (type) {
case "courses":
content = await this.client.listCourses();
break;
case "course": {
content = await this.client.getCourse(parseInt(id));
break;
}
case "assignments": {
content = await this.client.listAssignments(parseInt(id));
break;
}
case "users": {
content = await this.client.listUsers(parseInt(id));
break;
}
case "grades": {
content = await this.client.getCourseGrades(parseInt(id));
break;
}
case "quizzes": {
content = await this.client.listQuizzes(id);
break;
}
case "modules": {
content = await this.client.listModules(parseInt(id));
break;
}
case "discussion_topics": {
content = await this.client.listDiscussionTopics(parseInt(id));
break;
}
case "announcements": {
content = await this.client.listAnnouncements(id);
break;
}
default:
throw new Error(`Unknown resource type: ${type}`);
}
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(content, null, 2)
}]
};
} catch (error) {
console.error(`Error reading resource ${uri}:`, error);
throw error;
}
});
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const args = request.params.arguments || {};
switch (request.params.name) {
case "canvas_create_course": {
const courseArgs = args as unknown as CreateCourseArgs;
if (!courseArgs.name) {
throw new Error("Missing required field: name");
}
const course = await this.client.createCourse(courseArgs);
return {
content: [{ type: "text", text: JSON.stringify(course, null, 2) }]
};
}
case "canvas_update_course": {
const updateArgs = args as unknown as UpdateCourseArgs;
if (!updateArgs.course_id) {
throw new Error("Missing required field: course_id");
}
const updatedCourse = await this.client.updateCourse(updateArgs);
return {
content: [{ type: "text", text: JSON.stringify(updatedCourse, null, 2) }]
};
}
case "canvas_create_assignment": {
const assignmentArgs = args as unknown as CreateAssignmentArgs;
if (!assignmentArgs.course_id || !assignmentArgs.name) {
throw new Error("Missing required fields: course_id and name");
}
const assignment = await this.client.createAssignment(assignmentArgs);
return {
content: [{ type: "text", text: JSON.stringify(assignment, null, 2) }]
};
}
case "canvas_update_assignment": {
const updateAssignmentArgs = args as unknown as UpdateAssignmentArgs;
if (!updateAssignmentArgs.course_id || !updateAssignmentArgs.assignment_id) {
throw new Error("Missing required fields: course_id and assignment_id");
}
const updatedAssignment = await this.client.updateAssignment(updateAssignmentArgs);
return {
content: [{ type: "text", text: JSON.stringify(updatedAssignment, null, 2) }]
};
}
case "canvas_submit_grade": {
const gradeArgs = args as unknown as SubmitGradeArgs;
if (!gradeArgs.course_id || !gradeArgs.assignment_id ||
!gradeArgs.user_id || gradeArgs.grade === undefined) {
throw new Error("Missing required fields for grade submission");
}
const submission = await this.client.submitGrade(gradeArgs);
return {
content: [{ type: "text", text: JSON.stringify(submission, null, 2) }]
};
}
case "canvas_enroll_user": {
const enrollArgs = args as unknown as EnrollUserArgs;
if (!enrollArgs.course_id || !enrollArgs.user_id) {
throw new Error("Missing required fields: course_id and user_id");
}
const enrollment = await this.client.enrollUser(enrollArgs);
return {
content: [{ type: "text", text: JSON.stringify(enrollment, null, 2) }]
};
}
case "canvas_submit_assignment": {
const submitArgs = args as unknown as {
course_id: string;
assignment_id: number;
user_id: number;
submission_type: string;
body?: string;
};
const { course_id, assignment_id, user_id, submission_type, body } = submitArgs;
if (!course_id || !assignment_id || !user_id || !submission_type) {
throw new Error("Missing required fields for assignment submission");
}
const submission = await this.client.submitAssignment({
course_id,
assignment_id,
user_id,
submission_type,
body
});
return {
content: [{ type: "text", text: JSON.stringify(submission, null, 2) }]
};
}
case "canvas_list_quizzes": {
const { course_id } = args as { course_id: string };
if (!course_id) {
throw new Error("Missing required field: course_id");
}
const quizzes = await this.client.listQuizzes(course_id);
return {
content: [{ type: "text", text: JSON.stringify(quizzes, null, 2) }]
};
}
case "canvas_get_quiz": {
const { course_id, quiz_id } = args as { course_id: string; quiz_id: number };
if (!course_id || !quiz_id) {
throw new Error("Missing required fields: course_id and quiz_id");
}
const quiz = await this.client.getQuiz(course_id, quiz_id);
return {
content: [{ type: "text", text: JSON.stringify(quiz, null, 2) }]
};
}
case "canvas_create_quiz": {
const { course_id, title, quiz_type, time_limit, published, description, due_at } = args as {
course_id: number;
title: string;
quiz_type?: string;
time_limit?: number;
published?: boolean;
description?: string;
due_at?: string;
};
if (!course_id || !title) {
throw new Error("Missing required fields: course_id and title");
}
const quiz = await this.client.createQuiz(course_id, { title, quiz_type, time_limit, published, description, due_at });
return {
content: [{ type: "text", text: JSON.stringify(quiz, null, 2) }]
};
}
case "canvas_update_quiz": {
const { course_id, quiz_id, title, quiz_type, time_limit, published, description, due_at } = args as {
course_id: number;
quiz_id: number;
title?: string;
quiz_type?: string;
time_limit?: number;
published?: boolean;
description?: string;
due_at?: string;
};
if (!course_id || !quiz_id) {
throw new Error("Missing required fields: course_id and quiz_id");
}
const updatedQuiz = await this.client.updateQuiz(course_id, quiz_id, { title, quiz_type, time_limit, published, description, due_at });
return {
content: [{ type: "text", text: JSON.stringify(updatedQuiz, null, 2) }]
};
}
case "canvas_delete_quiz": {
const { course_id, quiz_id } = args as { course_id: number; quiz_id: number };
if (!course_id || !quiz_id) {
throw new Error("Missing required fields: course_id and quiz_id");
}
await this.client.deleteQuiz(course_id, quiz_id);
return {
content: [{ type: "text", text: `Quiz ${quiz_id} deleted successfully.` }]
};
}
case "canvas_list_modules": {
const { course_id } = args as { course_id: number };
if (!course_id) {
throw new Error("Missing required field: course_id");
}
const modules = await this.client.listModules(course_id);
return {
content: [{ type: "text", text: JSON.stringify(modules, null, 2) }]
};
}
case "canvas_get_module": {
const { course_id, module_id } = args as { course_id: number; module_id: number };
if (!course_id || !module_id) {
throw new Error("Missing required fields: course_id and module_id");
}
const module = await this.client.getModule(course_id, module_id);
return {
content: [{ type: "text", text: JSON.stringify(module, null, 2) }]
};
}
case "canvas_list_module_items": {
const { course_id, module_id } = args as { course_id: number; module_id: number };
if (!course_id || !module_id) {
throw new Error("Missing required fields: course_id and module_id");
}
const items = await this.client.listModuleItems(course_id, module_id);
return {
content: [{ type: "text", text: JSON.stringify(items, null, 2) }]
};
}
case "canvas_get_module_item": {
const { course_id, module_id, item_id } = args as { course_id: number; module_id: number; item_id: number };
if (!course_id || !module_id || !item_id) {
throw new Error("Missing required fields: course_id, module_id, and item_id");
}
const item = await this.client.getModuleItem(course_id, module_id, item_id);
return {
content: [{ type: "text", text: JSON.stringify(item, null, 2) }]
};
}
case "canvas_list_discussion_topics": {
const { course_id } = args as { course_id: number };
if (!course_id) {
throw new Error("Missing required field: course_id");
}
const topics = await this.client.listDiscussionTopics(course_id);
return {
content: [{ type: "text", text: JSON.stringify(topics, null, 2) }]
};
}
case "canvas_get_discussion_topic": {
const { course_id, topic_id } = args as { course_id: number; topic_id: number };
if (!course_id || !topic_id) {
throw new Error("Missing required fields: course_id and topic_id");
}
const topic = await this.client.getDiscussionTopic(course_id, topic_id);
return {
content: [{ type: "text", text: JSON.stringify(topic, null, 2) }]
};
}
case "canvas_list_announcements": {
const { course_id } = args as { course_id: string };
if (!course_id) {
throw new Error("Missing required field: course_id");
}
const announcements = await this.client.listAnnouncements(course_id);
return {
content: [{ type: "text", text: JSON.stringify(announcements, null, 2) }]
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error) {
console.error(`Error executing tool ${request.params.name}:`, error);
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Canvas MCP server running on stdio");
}
}
// Main entry point
async function main() {
// Get current file's directory in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Get all directories from PATH
const pathDirs = (process.env.PATH || '').split(path.delimiter);
// Create array of possible .env locations
const envPaths = [
'.env', // Current directory
'src/.env', // src directory
path.join(__dirname, '.env'), // Script directory
path.join(process.cwd(), '.env'), // Working directory
...pathDirs.map(dir => path.join(dir, '.env')), // All PATH directories
];
// Try loading from each possible location
let loaded = false;
for (const envPath of envPaths) {
const result = dotenv.config({ path: envPath });
if (result.parsed) {
console.error(`Loaded environment from: ${envPath}`);
loaded = true;
break;
}
}
if (!loaded) {
console.error('Warning: No .env file found in PATH or standard locations');
}
const token = process.env.CANVAS_API_TOKEN;
const domain = process.env.CANVAS_DOMAIN;
if (!token || !domain) {
console.error("Please set CANVAS_API_TOKEN and CANVAS_DOMAIN environment variables");
process.exit(1);
}
try {
const server = new CanvasMCPServer(token, domain);
await server.run();
} catch (error) {
console.error("Fatal error:", error);
process.exit(1);
}
}
main().catch(console.error);