Skip to main content
Glama

Canvas MCP Server V2.0

// src/client.ts import axios, { AxiosInstance, AxiosError } from 'axios'; import { CanvasCourse, CanvasAssignment, CanvasSubmission, CanvasUser, CanvasEnrollment, CreateCourseArgs, UpdateCourseArgs, CreateAssignmentArgs, UpdateAssignmentArgs, SubmitGradeArgs, EnrollUserArgs, CanvasAPIError, CanvasDiscussionTopic, CanvasModule, CanvasModuleItem, CanvasQuiz, CanvasAnnouncement, CanvasUserProfile, CanvasScope, CanvasAssignmentSubmission, CanvasPage, CanvasCalendarEvent, CanvasRubric, CanvasAssignmentGroup, CanvasConversation, CanvasNotification, CanvasFile, CanvasSyllabus, CanvasDashboard, SubmitAssignmentArgs, FileUploadArgs, CanvasAccount, CreateUserArgs, CanvasAccountReport, CreateReportArgs, ListAccountCoursesArgs, ListAccountUsersArgs } from './types.js'; export class CanvasClient { private client: AxiosInstance; private baseURL: string; private maxRetries: number = 3; private retryDelay: number = 1000; constructor(token: string, domain: string, options?: { maxRetries?: number; retryDelay?: number }) { this.baseURL = `https://${domain}/api/v1`; this.maxRetries = options?.maxRetries ?? 3; this.retryDelay = options?.retryDelay ?? 1000; this.client = axios.create({ baseURL: this.baseURL, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 30000 // 30 second timeout }); this.setupInterceptors(); } private setupInterceptors(): void { // Request interceptor for logging this.client.interceptors.request.use( (config) => { console.error(`[Canvas API] ${config.method?.toUpperCase()} ${config.url}`); return config; }, (error) => { console.error('[Canvas API] Request error:', error.message || error); return Promise.reject(error); } ); // Response interceptor for pagination and retry logic this.client.interceptors.response.use( async (response) => { const { headers, data } = response; const linkHeader = headers.link; const contentType = headers['content-type'] || ''; // Only handle pagination for JSON responses if (Array.isArray(data) && linkHeader && contentType.includes('application/json')) { let allData = [...data]; let nextUrl = this.getNextPageUrl(linkHeader); while (nextUrl) { const nextResponse = await this.client.get(nextUrl); allData = [...allData, ...nextResponse.data]; nextUrl = this.getNextPageUrl(nextResponse.headers.link); } response.data = allData; } return response; }, async (error: AxiosError) => { const config = error.config as any; // Retry logic for specific errors if (this.shouldRetry(error) && config && config.__retryCount < this.maxRetries) { config.__retryCount = config.__retryCount || 0; config.__retryCount++; const delay = this.retryDelay * Math.pow(2, config.__retryCount - 1); // Exponential backoff console.error(`[Canvas API] Retrying request (${config.__retryCount}/${this.maxRetries}) after ${delay}ms`); await this.sleep(delay); return this.client.request(config); } // Transform error with better handling for non-JSON responses if (error.response) { const { status, data, headers } = error.response; const contentType = headers?.['content-type'] || 'unknown'; console.error(`[Canvas API] Error response: ${status}, Content-Type: ${contentType}, Data type: ${typeof data}`); let errorMessage: string; try { // Check if data is already a string (HTML error pages, plain text, etc.) if (typeof data === 'string') { errorMessage = data.length > 200 ? data.substring(0, 200) + '...' : data; } else if (data && typeof data === 'object') { // Handle structured Canvas API error responses if ((data as any)?.message) { errorMessage = (data as any).message; } else if ((data as any)?.errors && Array.isArray((data as any).errors)) { errorMessage = (data as any).errors.map((err: any) => err.message || err).join(', '); } else { errorMessage = JSON.stringify(data); } } else { errorMessage = String(data); } } catch (jsonError) { // Fallback if JSON operations fail errorMessage = String(data); } throw new CanvasAPIError( `Canvas API Error (${status}): ${errorMessage}`, status, data ); } // Handle network errors or other issues if (error.request) { console.error('[Canvas API] Network error - no response received:', error.message); throw new CanvasAPIError( `Network error: ${error.message}`, 0, null ); } console.error('[Canvas API] Unexpected error:', error.message); throw error; } ); } private shouldRetry(error: AxiosError): boolean { if (!error.response) return true; // Network errors const status = error.response.status; return status === 429 || status >= 500; // Rate limit or server errors } private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } private getNextPageUrl(linkHeader: string): string | null { const links = linkHeader.split(','); const nextLink = links.find(link => link.includes('rel="next"')); if (!nextLink) return null; const match = nextLink.match(/<(.+?)>/); return match ? match[1] : null; } // --------------------- // HEALTH CHECK // --------------------- async healthCheck(): Promise<{ status: 'ok' | 'error'; timestamp: string; user?: any }> { try { const user = await this.getUserProfile(); return { status: 'ok', timestamp: new Date().toISOString(), user: { id: user.id, name: user.name } }; } catch (error) { return { status: 'error', timestamp: new Date().toISOString() }; } } // --------------------- // COURSES (Enhanced) // --------------------- async listCourses(includeEnded: boolean = false): Promise<CanvasCourse[]> { const params: any = { include: ['total_students', 'teachers', 'term', 'course_progress'] }; if (!includeEnded) { params.state = ['available', 'completed']; } const response = await this.client.get('/courses', { params }); return response.data; } async getCourse(courseId: number): Promise<CanvasCourse> { const response = await this.client.get(`/courses/${courseId}`, { params: { include: ['total_students', 'teachers', 'term', 'course_progress', 'sections', 'syllabus_body'] } }); return response.data; } async createCourse(args: CreateCourseArgs): Promise<CanvasCourse> { const { account_id, ...courseData } = args; const response = await this.client.post(`/accounts/${account_id}/courses`, { course: courseData }); return response.data; } async updateCourse(args: UpdateCourseArgs): Promise<CanvasCourse> { const { course_id, ...courseData } = args; const response = await this.client.put(`/courses/${course_id}`, { course: courseData }); return response.data; } async deleteCourse(courseId: number): Promise<void> { await this.client.delete(`/courses/${courseId}`); } // --------------------- // ASSIGNMENTS (Enhanced) // --------------------- async listAssignments(courseId: number, includeSubmissions: boolean = false): Promise<CanvasAssignment[]> { const params: any = { include: ['assignment_group', 'rubric', 'due_at'] }; if (includeSubmissions) { params.include.push('submission'); } const response = await this.client.get(`/courses/${courseId}/assignments`, { params }); return response.data; } async getAssignment(courseId: number, assignmentId: number, includeSubmission: boolean = false): Promise<CanvasAssignment> { const params: any = { include: ['assignment_group', 'rubric'] }; if (includeSubmission) { params.include.push('submission'); } const response = await this.client.get(`/courses/${courseId}/assignments/${assignmentId}`, { params }); return response.data; } async createAssignment(args: CreateAssignmentArgs): Promise<CanvasAssignment> { const { course_id, ...assignmentData } = args; const response = await this.client.post(`/courses/${course_id}/assignments`, { assignment: assignmentData }); return response.data; } async updateAssignment(args: UpdateAssignmentArgs): Promise<CanvasAssignment> { const { course_id, assignment_id, ...assignmentData } = args; const response = await this.client.put( `/courses/${course_id}/assignments/${assignment_id}`, { assignment: assignmentData } ); return response.data; } async deleteAssignment(courseId: number, assignmentId: number): Promise<void> { await this.client.delete(`/courses/${courseId}/assignments/${assignmentId}`); } // --------------------- // ASSIGNMENT GROUPS // --------------------- async listAssignmentGroups(courseId: number): Promise<CanvasAssignmentGroup[]> { const response = await this.client.get(`/courses/${courseId}/assignment_groups`, { params: { include: ['assignments'] } }); return response.data; } async getAssignmentGroup(courseId: number, groupId: number): Promise<CanvasAssignmentGroup> { const response = await this.client.get(`/courses/${courseId}/assignment_groups/${groupId}`, { params: { include: ['assignments'] } }); return response.data; } // --------------------- // SUBMISSIONS (Enhanced for Students) // --------------------- async getSubmissions(courseId: number, assignmentId: number): Promise<CanvasSubmission[]> { const response = await this.client.get( `/courses/${courseId}/assignments/${assignmentId}/submissions`, { params: { include: ['submission_comments', 'rubric_assessment', 'assignment'] } } ); return response.data; } async getSubmission(courseId: number, assignmentId: number, userId: number | 'self' = 'self'): Promise<CanvasSubmission> { const response = await this.client.get( `/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`, { params: { include: ['submission_comments', 'rubric_assessment', 'assignment'] } } ); return response.data; } async submitGrade(args: SubmitGradeArgs): Promise<CanvasSubmission> { const { course_id, assignment_id, user_id, grade, comment } = args; const response = await this.client.put( `/courses/${course_id}/assignments/${assignment_id}/submissions/${user_id}`, { submission: { posted_grade: grade, comment: comment ? { text_comment: comment } : undefined } }); return response.data; } // Student submission with file support async submitAssignment(args: SubmitAssignmentArgs): Promise<CanvasAssignmentSubmission> { const { course_id, assignment_id, submission_type, body, url, file_ids } = args; const submissionData: any = { submission_type }; if (body) submissionData.body = body; if (url) submissionData.url = url; if (file_ids && file_ids.length > 0) submissionData.file_ids = file_ids; const response = await this.client.post( `/courses/${course_id}/assignments/${assignment_id}/submissions`, { submission: submissionData } ); return response.data; } // --------------------- // FILES (Enhanced) // --------------------- async listFiles(courseId: number, folderId?: number): Promise<CanvasFile[]> { const endpoint = folderId ? `/folders/${folderId}/files` : `/courses/${courseId}/files`; const response = await this.client.get(endpoint); return response.data; } async getFile(fileId: number): Promise<CanvasFile> { const response = await this.client.get(`/files/${fileId}`); return response.data; } async uploadFile(args: FileUploadArgs): Promise<CanvasFile> { const { course_id, folder_id, name, size } = args; // Step 1: Get upload URL const uploadEndpoint = folder_id ? `/folders/${folder_id}/files` : `/courses/${course_id}/files`; const uploadResponse = await this.client.post(uploadEndpoint, { name, size, content_type: args.content_type || 'application/octet-stream' }); // Note: Actual file upload would require multipart form data handling // This is a simplified version - in practice, you'd need to handle the // two-step upload process Canvas uses return uploadResponse.data; } async listFolders(courseId: number): Promise<any[]> { const response = await this.client.get(`/courses/${courseId}/folders`); return response.data; } // --------------------- // PAGES // --------------------- async listPages(courseId: number): Promise<CanvasPage[]> { const response = await this.client.get(`/courses/${courseId}/pages`); return response.data; } async getPage(courseId: number, pageUrl: string): Promise<CanvasPage> { const response = await this.client.get(`/courses/${courseId}/pages/${pageUrl}`); return response.data; } // --------------------- // CALENDAR EVENTS // --------------------- async listCalendarEvents(startDate?: string, endDate?: string): Promise<CanvasCalendarEvent[]> { const params: any = { type: 'event', all_events: true }; if (startDate) params.start_date = startDate; if (endDate) params.end_date = endDate; const response = await this.client.get('/calendar_events', { params }); return response.data; } async getUpcomingAssignments(limit: number = 10): Promise<CanvasAssignment[]> { const response = await this.client.get('/users/self/upcoming_events', { params: { limit } }); return response.data.filter((event: any) => event.assignment); } // --------------------- // RUBRICS // --------------------- async listRubrics(courseId: number): Promise<CanvasRubric[]> { const response = await this.client.get(`/courses/${courseId}/rubrics`); return response.data; } async getRubric(courseId: number, rubricId: number): Promise<CanvasRubric> { const response = await this.client.get(`/courses/${courseId}/rubrics/${rubricId}`); return response.data; } // --------------------- // DASHBOARD // --------------------- async getDashboard(): Promise<CanvasDashboard> { const response = await this.client.get('/users/self/dashboard'); return response.data; } async getDashboardCards(): Promise<any[]> { const response = await this.client.get('/dashboard/dashboard_cards'); return response.data; } // --------------------- // SYLLABUS // --------------------- async getSyllabus(courseId: number): Promise<CanvasSyllabus> { const response = await this.client.get(`/courses/${courseId}`, { params: { include: ['syllabus_body'] } }); return { course_id: courseId, syllabus_body: response.data.syllabus_body }; } // --------------------- // CONVERSATIONS/MESSAGING // --------------------- async listConversations(): Promise<CanvasConversation[]> { const response = await this.client.get('/conversations'); return response.data; } async getConversation(conversationId: number): Promise<CanvasConversation> { const response = await this.client.get(`/conversations/${conversationId}`); return response.data; } async createConversation(recipients: string[], body: string, subject?: string): Promise<CanvasConversation> { const response = await this.client.post('/conversations', { recipients, body, subject }); return response.data; } // --------------------- // NOTIFICATIONS // --------------------- async listNotifications(): Promise<CanvasNotification[]> { const response = await this.client.get('/users/self/activity_stream'); return response.data; } // --------------------- // USERS AND ENROLLMENTS (Enhanced) // --------------------- async listUsers(courseId: number): Promise<CanvasUser[]> { const response = await this.client.get(`/courses/${courseId}/users`, { params: { include: ['email', 'enrollments', 'avatar_url'] } }); return response.data; } async getEnrollments(courseId: number): Promise<CanvasEnrollment[]> { const response = await this.client.get(`/courses/${courseId}/enrollments`); return response.data; } async enrollUser(args: EnrollUserArgs): Promise<CanvasEnrollment> { const { course_id, user_id, role = 'StudentEnrollment', enrollment_state = 'active' } = args; const response = await this.client.post(`/courses/${course_id}/enrollments`, { enrollment: { user_id, type: role, enrollment_state } }); return response.data; } async unenrollUser(courseId: number, enrollmentId: number): Promise<void> { await this.client.delete(`/courses/${courseId}/enrollments/${enrollmentId}`); } // --------------------- // GRADES (Enhanced) // --------------------- async getCourseGrades(courseId: number): Promise<CanvasEnrollment[]> { const response = await this.client.get(`/courses/${courseId}/enrollments`, { params: { include: ['grades', 'observed_users'] } }); return response.data; } async getUserGrades(): Promise<any> { const response = await this.client.get('/users/self/grades'); return response.data; } // --------------------- // USER PROFILE (Enhanced) // --------------------- async getUserProfile(): Promise<CanvasUserProfile> { const response = await this.client.get('/users/self/profile'); return response.data; } async updateUserProfile(profileData: Partial<CanvasUserProfile>): Promise<CanvasUserProfile> { const response = await this.client.put('/users/self', { user: profileData }); return response.data; } // --------------------- // STUDENT COURSES (Enhanced) // --------------------- async listStudentCourses(): Promise<CanvasCourse[]> { const response = await this.client.get('/courses', { params: { include: ['enrollments', 'total_students', 'term', 'course_progress'], enrollment_state: 'active' } }); return response.data; } // --------------------- // MODULES (Enhanced) // --------------------- async listModules(courseId: number): Promise<CanvasModule[]> { const response = await this.client.get(`/courses/${courseId}/modules`, { params: { include: ['items'] } }); return response.data; } async getModule(courseId: number, moduleId: number): Promise<CanvasModule> { const response = await this.client.get(`/courses/${courseId}/modules/${moduleId}`, { params: { include: ['items'] } }); return response.data; } async listModuleItems(courseId: number, moduleId: number): Promise<CanvasModuleItem[]> { const response = await this.client.get(`/courses/${courseId}/modules/${moduleId}/items`, { params: { include: ['content_details'] } }); return response.data; } async getModuleItem(courseId: number, moduleId: number, itemId: number): Promise<CanvasModuleItem> { const response = await this.client.get(`/courses/${courseId}/modules/${moduleId}/items/${itemId}`, { params: { include: ['content_details'] } }); return response.data; } async markModuleItemComplete(courseId: number, moduleId: number, itemId: number): Promise<void> { await this.client.put(`/courses/${courseId}/modules/${moduleId}/items/${itemId}/done`); } // --------------------- // DISCUSSION TOPICS (Enhanced) // --------------------- async listDiscussionTopics(courseId: number): Promise<CanvasDiscussionTopic[]> { const response = await this.client.get(`/courses/${courseId}/discussion_topics`, { params: { include: ['assignment'] } }); return response.data; } async getDiscussionTopic(courseId: number, topicId: number): Promise<CanvasDiscussionTopic> { const response = await this.client.get(`/courses/${courseId}/discussion_topics/${topicId}`, { params: { include: ['assignment'] } }); return response.data; } async postToDiscussion(courseId: number, topicId: number, message: string): Promise<any> { const response = await this.client.post(`/courses/${courseId}/discussion_topics/${topicId}/entries`, { message }); return response.data; } // --------------------- // ANNOUNCEMENTS (Enhanced) // --------------------- async listAnnouncements(courseId: string): Promise<CanvasAnnouncement[]> { const response = await this.client.get(`/courses/${courseId}/discussion_topics`, { params: { type: 'announcement', include: ['assignment'] } }); return response.data; } // --------------------- // QUIZZES (Enhanced) // --------------------- async listQuizzes(courseId: string): Promise<CanvasQuiz[]> { const response = await this.client.get(`/courses/${courseId}/quizzes`); return response.data; } async getQuiz(courseId: string, quizId: number): Promise<CanvasQuiz> { const response = await this.client.get(`/courses/${courseId}/quizzes/${quizId}`); return response.data; } async createQuiz(courseId: number, quizData: Partial<CanvasQuiz>): Promise<CanvasQuiz> { const response = await this.client.post(`/courses/${courseId}/quizzes`, { quiz: quizData }); return response.data; } async updateQuiz(courseId: number, quizId: number, quizData: Partial<CanvasQuiz>): Promise<CanvasQuiz> { const response = await this.client.put(`/courses/${courseId}/quizzes/${quizId}`, { quiz: quizData }); return response.data; } async deleteQuiz(courseId: number, quizId: number): Promise<void> { await this.client.delete(`/courses/${courseId}/quizzes/${quizId}`); } async startQuizAttempt(courseId: number, quizId: number): Promise<any> { const response = await this.client.post(`/courses/${courseId}/quizzes/${quizId}/submissions`); return response.data; } async submitQuizAttempt(courseId: number, quizId: number, submissionId: number, answers: any): Promise<any> { const response = await this.client.post( `/courses/${courseId}/quizzes/${quizId}/submissions/${submissionId}/complete`, { quiz_submissions: [{ attempt: 1, questions: answers }] } ); return response.data; } // --------------------- // SCOPES (Enhanced) // --------------------- async listTokenScopes(accountId: number, groupBy?: string): Promise<CanvasScope[]> { const params: Record<string, string> = {}; if (groupBy) { params.group_by = groupBy; } const response = await this.client.get(`/accounts/${accountId}/scopes`, { params }); return response.data; } // --------------------- // ACCOUNT MANAGEMENT (New) // --------------------- async getAccount(accountId: number): Promise<CanvasAccount> { const response = await this.client.get(`/accounts/${accountId}`); return response.data; } async listAccountCourses(args: ListAccountCoursesArgs): Promise<CanvasCourse[]> { const { account_id, ...params } = args; const response = await this.client.get(`/accounts/${account_id}/courses`, { params }); return response.data; } async listAccountUsers(args: ListAccountUsersArgs): Promise<CanvasUser[]> { const { account_id, ...params } = args; const response = await this.client.get(`/accounts/${account_id}/users`, { params }); return response.data; } async createUser(args: CreateUserArgs): Promise<CanvasUser> { const { account_id, ...userData } = args; const response = await this.client.post(`/accounts/${account_id}/users`, userData); return response.data; } async listSubAccounts(accountId: number): Promise<CanvasAccount[]> { const response = await this.client.get(`/accounts/${accountId}/sub_accounts`); return response.data; } // --------------------- // ACCOUNT REPORTS (New) // --------------------- async getAccountReports(accountId: number): Promise<any[]> { const response = await this.client.get(`/accounts/${accountId}/reports`); return response.data; } async createAccountReport(args: CreateReportArgs): Promise<CanvasAccountReport> { const { account_id, report, parameters } = args; const response = await this.client.post(`/accounts/${account_id}/reports/${report}`, { parameters: parameters || {} }); return response.data; } async getAccountReport(accountId: number, reportType: string, reportId: number): Promise<CanvasAccountReport> { const response = await this.client.get(`/accounts/${accountId}/reports/${reportType}/${reportId}`); return response.data; } }

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/DMontgomery40/mcp-canvas-lms'

If you have feedback or need assistance with the MCP directory API, please join our Discord server