@charlie.act7/canvas-mcp-server 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,459 @@
1
+ import axios from 'axios';
2
+ export class CanvasClient {
3
+ client;
4
+ token;
5
+ domain;
6
+ constructor(token, domain) {
7
+ this.token = token;
8
+ this.domain = domain;
9
+ this.client = this.createAxiosInstance(token, domain);
10
+ }
11
+ createAxiosInstance(token, domain) {
12
+ const baseURL = `https://${domain}/api/v1`;
13
+ return axios.create({
14
+ baseURL,
15
+ headers: {
16
+ 'Authorization': `Bearer ${token}`,
17
+ 'Content-Type': 'application/json'
18
+ }
19
+ });
20
+ }
21
+ updateConfig(token, domain) {
22
+ this.token = token;
23
+ this.domain = domain;
24
+ this.client = this.createAxiosInstance(token, domain);
25
+ }
26
+ parseLinkHeader(header) {
27
+ if (!header)
28
+ return {};
29
+ const links = {};
30
+ const parts = header.split(',');
31
+ parts.forEach(part => {
32
+ const section = part.split(';');
33
+ if (section.length < 2)
34
+ return;
35
+ const url = section[0].replace(/<(.*)>/, '$1').trim();
36
+ const name = section[1].replace(/rel="?([^"]+)"?/, '$1').trim();
37
+ links[name] = url;
38
+ });
39
+ return links;
40
+ }
41
+ async getAllPages(initialUrl, params) {
42
+ let allResults = [];
43
+ let nextUrl = initialUrl;
44
+ while (nextUrl) {
45
+ const response = await this.client.get(nextUrl, { params: nextUrl === initialUrl ? params : undefined });
46
+ if (response.data) {
47
+ allResults = allResults.concat(response.data);
48
+ }
49
+ const links = this.parseLinkHeader(response.headers['link']);
50
+ nextUrl = links['next'] || null;
51
+ }
52
+ return allResults;
53
+ }
54
+ async getCourses() {
55
+ return this.getAllPages('courses', {
56
+ include: ['term'],
57
+ enrollment_state: 'active',
58
+ per_page: 100
59
+ });
60
+ }
61
+ async getModules(courseId) {
62
+ return this.getAllPages(`courses/${courseId}/modules`, {
63
+ include: ['items']
64
+ });
65
+ }
66
+ async getAssignments(courseId) {
67
+ return this.getAllPages(`courses/${courseId}/assignments`, {
68
+ per_page: 100
69
+ });
70
+ }
71
+ async getAssignmentGroups(courseId) {
72
+ return this.getAllPages(`courses/${courseId}/assignment_groups`, {
73
+ per_page: 100
74
+ });
75
+ }
76
+ async getQuizzes(courseId) {
77
+ return this.getAllPages(`courses/${courseId}/quizzes`, {
78
+ per_page: 100
79
+ });
80
+ }
81
+ async getAssignment(courseId, assignmentId) {
82
+ const response = await this.client.get(`courses/${courseId}/assignments/${assignmentId}`, {
83
+ params: {
84
+ include: ['submission', 'rubric_settings', 'overrides']
85
+ }
86
+ });
87
+ return response.data;
88
+ }
89
+ async updateAssignmentDates(courseId, assignmentId, dates) {
90
+ const response = await this.client.put(`courses/${courseId}/assignments/${assignmentId}`, {
91
+ assignment: dates
92
+ });
93
+ return response.data;
94
+ }
95
+ async createAssignment(courseId, assignment) {
96
+ const response = await this.client.post(`courses/${courseId}/assignments`, {
97
+ assignment: assignment
98
+ });
99
+ return response.data;
100
+ }
101
+ async updateAssignment(courseId, assignmentId, assignment) {
102
+ const response = await this.client.put(`courses/${courseId}/assignments/${assignmentId}`, {
103
+ assignment: assignment
104
+ });
105
+ return response.data;
106
+ }
107
+ async getQuiz(courseId, quizId) {
108
+ const response = await this.client.get(`courses/${courseId}/quizzes/${quizId}`);
109
+ return response.data;
110
+ }
111
+ async updateQuizDates(courseId, quizId, dates) {
112
+ const response = await this.client.put(`courses/${courseId}/quizzes/${quizId}`, {
113
+ quiz: dates
114
+ });
115
+ return response.data;
116
+ }
117
+ async getSubmissions(courseId, assignmentId) {
118
+ return this.getAllPages(`courses/${courseId}/assignments/${assignmentId}/submissions`, {
119
+ include: ['user'],
120
+ per_page: 100
121
+ });
122
+ }
123
+ async gradeSubmission(courseId, assignmentId, userId, grade, comment, rubric_assessment) {
124
+ const url = `courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`;
125
+ const data = {
126
+ submission: {
127
+ posted_grade: grade
128
+ }
129
+ };
130
+ if (comment) {
131
+ data.comment = {
132
+ text_comment: comment
133
+ };
134
+ }
135
+ if (rubric_assessment) {
136
+ data.rubric_assessment = rubric_assessment;
137
+ }
138
+ const response = await this.client.put(url, data);
139
+ return response.data;
140
+ }
141
+ async getSingleSubmission(courseId, assignmentId, userId) {
142
+ const url = `courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`;
143
+ const response = await this.client.get(url, {
144
+ params: {
145
+ include: ['submission_history', 'submission_comments', 'rubric_assessment', 'visibility', 'user']
146
+ }
147
+ });
148
+ return response.data;
149
+ }
150
+ async getEnrollments(courseId) {
151
+ return this.getAllPages(`courses/${courseId}/users`, {
152
+ enrollment_type: ['student'],
153
+ include: ['email', 'enrollments'],
154
+ per_page: 100
155
+ });
156
+ }
157
+ async getStudentInCourse(courseId, studentId) {
158
+ const response = await this.client.get(`courses/${courseId}/users/${studentId}`, {
159
+ params: {
160
+ include: ['email', 'enrollments']
161
+ }
162
+ });
163
+ return response.data;
164
+ }
165
+ async getStudentCourseSubmissions(courseId, studentId) {
166
+ return this.getAllPages(`courses/${courseId}/students/submissions`, {
167
+ student_ids: [studentId],
168
+ include: ['assignment'],
169
+ per_page: 100
170
+ });
171
+ }
172
+ async getPages(courseId) {
173
+ return this.getAllPages(`courses/${courseId}/pages`);
174
+ }
175
+ async getPage(courseId, pageUrlOrId) {
176
+ const response = await this.client.get(`courses/${courseId}/pages/${pageUrlOrId}`);
177
+ return response.data;
178
+ }
179
+ async getFiles(courseId) {
180
+ return this.getAllPages(`courses/${courseId}/files`);
181
+ }
182
+ async getAnnouncements(courseIds) {
183
+ return this.getAllPages('announcements', {
184
+ context_codes: courseIds.map(id => `course_${id}`)
185
+ });
186
+ }
187
+ async deleteSubmissionComment(courseId, assignmentId, userId, commentId) {
188
+ const url = `courses/${courseId}/assignments/${assignmentId}/submissions/${userId}/comments/${commentId}`;
189
+ await this.client.delete(url);
190
+ return { deleted: true, comment_id: commentId };
191
+ }
192
+ async getSubmissionComments(courseId, assignmentId, userId) {
193
+ return this.getSingleSubmission(courseId, assignmentId, userId);
194
+ }
195
+ async getDiscussionTopics(courseId) {
196
+ return this.getAllPages(`courses/${courseId}/discussion_topics`);
197
+ }
198
+ async getDiscussionEntries(courseId, topicId) {
199
+ return this.getAllPages(`courses/${courseId}/discussion_topics/${topicId}/entries`);
200
+ }
201
+ async postDiscussionReply(courseId, topicId, message) {
202
+ const url = `courses/${courseId}/discussion_topics/${topicId}/entries`;
203
+ const response = await this.client.post(url, { message });
204
+ return response.data;
205
+ }
206
+ async postAnnouncement(courseId, title, message) {
207
+ // Announcements are technically discussion topics with is_announcement=true
208
+ const url = `courses/${courseId}/discussion_topics`;
209
+ const response = await this.client.post(url, {
210
+ title,
211
+ message,
212
+ is_announcement: true
213
+ });
214
+ return response.data;
215
+ }
216
+ // --- Quiz Questions ---
217
+ async listQuizQuestions(courseId, quizId) {
218
+ return this.getAllPages(`courses/${courseId}/quizzes/${quizId}/questions`, {
219
+ per_page: 100
220
+ });
221
+ }
222
+ async getQuizQuestion(courseId, quizId, questionId) {
223
+ const response = await this.client.get(`courses/${courseId}/quizzes/${quizId}/questions/${questionId}`);
224
+ return response.data;
225
+ }
226
+ async createQuizQuestion(courseId, quizId, data) {
227
+ const response = await this.client.post(`courses/${courseId}/quizzes/${quizId}/questions`, { question: data });
228
+ return response.data;
229
+ }
230
+ async updateQuizQuestion(courseId, quizId, questionId, data) {
231
+ const response = await this.client.put(`courses/${courseId}/quizzes/${quizId}/questions/${questionId}`, { question: data });
232
+ return response.data;
233
+ }
234
+ async deleteQuizQuestion(courseId, quizId, questionId) {
235
+ await this.client.delete(`courses/${courseId}/quizzes/${quizId}/questions/${questionId}`);
236
+ return { deleted: true };
237
+ }
238
+ // --- Quiz Groups ---
239
+ async createQuizGroup(courseId, quizId, groupData) {
240
+ const response = await this.client.post(`courses/${courseId}/quizzes/${quizId}/groups`, { quiz_groups: [groupData] });
241
+ return response.data.quiz_groups[0];
242
+ }
243
+ async createPage(courseId, title, body, published = false) {
244
+ const response = await this.client.post(`courses/${courseId}/pages`, {
245
+ wiki_page: {
246
+ title,
247
+ body,
248
+ published
249
+ }
250
+ });
251
+ return response.data;
252
+ }
253
+ async updatePage(courseId, pageUrlOrId, data) {
254
+ const response = await this.client.put(`courses/${courseId}/pages/${pageUrlOrId}`, {
255
+ wiki_page: data
256
+ });
257
+ return response.data;
258
+ }
259
+ // --- Modules ---
260
+ async createModule(courseId, name, published = false, position) {
261
+ const response = await this.client.post(`courses/${courseId}/modules`, {
262
+ module: {
263
+ name,
264
+ published,
265
+ position
266
+ }
267
+ });
268
+ return response.data;
269
+ }
270
+ async updateModule(courseId, moduleId, data) {
271
+ const response = await this.client.put(`courses/${courseId}/modules/${moduleId}`, {
272
+ module: data
273
+ });
274
+ return response.data;
275
+ }
276
+ async deleteModule(courseId, moduleId) {
277
+ await this.client.delete(`courses/${courseId}/modules/${moduleId}`);
278
+ return { deleted: true };
279
+ }
280
+ // --- Module Items ---
281
+ async createModuleItem(courseId, moduleId, item) {
282
+ const response = await this.client.post(`courses/${courseId}/modules/${moduleId}/items`, {
283
+ module_item: item
284
+ });
285
+ return response.data;
286
+ }
287
+ async updateModuleItem(courseId, moduleId, itemId, data) {
288
+ const response = await this.client.put(`courses/${courseId}/modules/${moduleId}/items/${itemId}`, {
289
+ module_item: data
290
+ });
291
+ return response.data;
292
+ }
293
+ async deleteModuleItem(courseId, moduleId, itemId) {
294
+ await this.client.delete(`courses/${courseId}/modules/${moduleId}/items/${itemId}`);
295
+ return { deleted: true };
296
+ }
297
+ // --- File Uploads ---
298
+ async uploadFile(courseId, filePath, fileName, contentType, parentFolderId) {
299
+ // Step 1: Pre-flight request to Canvas
300
+ const preflightData = {
301
+ name: fileName,
302
+ content_type: contentType
303
+ };
304
+ if (parentFolderId) {
305
+ preflightData.parent_folder_id = parentFolderId;
306
+ }
307
+ const preflightResponse = await this.client.post(`courses/${courseId}/files`, preflightData);
308
+ const { upload_url, upload_params } = preflightResponse.data;
309
+ // Step 2: Upload to S3 (or other storage)
310
+ // Note: Using dynamic import for 'fs' and 'form-data' to avoid issues in some environments
311
+ const fs = await import('node:fs');
312
+ const FormData = (await import('form-data')).default;
313
+ const form = new FormData();
314
+ Object.entries(upload_params).forEach(([key, value]) => {
315
+ form.append(key, value);
316
+ });
317
+ form.append('file', fs.createReadStream(filePath));
318
+ const uploadResponse = await axios.post(upload_url, form, {
319
+ headers: form.getHeaders()
320
+ });
321
+ // Step 3: Handle redirection if necessary (Canvas sometimes returns a 303 or a Location header)
322
+ if (uploadResponse.status === 301 || uploadResponse.status === 201 || uploadResponse.headers.location) {
323
+ const finalUrl = uploadResponse.headers.location || uploadResponse.data.location;
324
+ if (finalUrl) {
325
+ const finalResponse = await this.client.get(finalUrl);
326
+ return finalResponse.data;
327
+ }
328
+ }
329
+ return uploadResponse.data;
330
+ }
331
+ async createQuiz(courseId, quiz) {
332
+ const response = await this.client.post(`courses/${courseId}/quizzes`, {
333
+ quiz: quiz
334
+ });
335
+ return response.data;
336
+ }
337
+ async updateQuiz(courseId, quizId, quiz) {
338
+ const response = await this.client.put(`courses/${courseId}/quizzes/${quizId}`, {
339
+ quiz: quiz
340
+ });
341
+ return response.data;
342
+ }
343
+ // --- Folders ---
344
+ async getFolders(courseId) {
345
+ return this.getAllPages(`courses/${courseId}/folders`);
346
+ }
347
+ async getFolder(folderId) {
348
+ const response = await this.client.get(`folders/${folderId}`);
349
+ return response.data;
350
+ }
351
+ async createFolder(courseId, name, parentFolderId) {
352
+ const data = { name };
353
+ if (parentFolderId) {
354
+ data.parent_folder_id = parentFolderId;
355
+ }
356
+ const response = await this.client.post(`courses/${courseId}/folders`, data);
357
+ return response.data;
358
+ }
359
+ async updateFolder(folderId, data) {
360
+ const response = await this.client.put(`folders/${folderId}`, data);
361
+ return response.data;
362
+ }
363
+ async deleteFolder(folderId, force = false) {
364
+ await this.client.delete(`folders/${folderId}`, { params: { force } });
365
+ return { deleted: true };
366
+ }
367
+ // --- Files (More) ---
368
+ async getFile(fileId) {
369
+ const response = await this.client.get(`files/${fileId}`);
370
+ return response.data;
371
+ }
372
+ async updateFile(fileId, data) {
373
+ const response = await this.client.put(`files/${fileId}`, data);
374
+ return response.data;
375
+ }
376
+ async deleteFile(fileId) {
377
+ await this.client.delete(`files/${fileId}`);
378
+ return { deleted: true };
379
+ }
380
+ // --- Rubrics ---
381
+ async createRubric(courseId, rubricData, rubricAssociationData) {
382
+ const payload = { rubric: rubricData };
383
+ if (rubricAssociationData) {
384
+ payload.rubric_association = rubricAssociationData;
385
+ }
386
+ const response = await this.client.post(`courses/${courseId}/rubrics`, payload);
387
+ return response.data;
388
+ }
389
+ async createRubricAssociation(courseId, associationData) {
390
+ const response = await this.client.post(`courses/${courseId}/rubric_associations`, {
391
+ rubric_association: associationData
392
+ });
393
+ return response.data;
394
+ }
395
+ // --- Appointment Groups ---
396
+ async listAppointmentGroups(scope = 'all', include) {
397
+ return this.getAllPages('appointment_groups', {
398
+ scope,
399
+ include
400
+ });
401
+ }
402
+ async getAppointmentGroup(id, include) {
403
+ const response = await this.client.get(`appointment_groups/${id}`, {
404
+ params: { include }
405
+ });
406
+ return response.data;
407
+ }
408
+ async createAppointmentGroup(data) {
409
+ const response = await this.client.post('appointment_groups', {
410
+ appointment_group: data
411
+ });
412
+ return response.data;
413
+ }
414
+ async updateAppointmentGroup(id, data) {
415
+ const response = await this.client.put(`appointment_groups/${id}`, {
416
+ appointment_group: data
417
+ });
418
+ return response.data;
419
+ }
420
+ async deleteAppointmentGroup(id, cancelReason) {
421
+ await this.client.delete(`appointment_groups/${id}`, {
422
+ params: { cancel_reason: cancelReason }
423
+ });
424
+ return { deleted: true };
425
+ }
426
+ async listAppointmentGroupUsers(id) {
427
+ return this.getAllPages(`appointment_groups/${id}/users`);
428
+ }
429
+ async listAppointmentGroupGroups(id) {
430
+ return this.getAllPages(`appointment_groups/${id}/groups`);
431
+ }
432
+ async getNextAppointment() {
433
+ const response = await this.client.get('appointment_groups/next_appointment');
434
+ return response.data;
435
+ }
436
+ // --- Groups and Group Categories ---
437
+ async getGroupCategories(courseId) {
438
+ return this.getAllPages(`courses/${courseId}/group_categories`);
439
+ }
440
+ async createGroupCategory(courseId, data) {
441
+ const response = await this.client.post(`courses/${courseId}/group_categories`, data);
442
+ return response.data;
443
+ }
444
+ async createGroup(groupCategoryId, data) {
445
+ const response = await this.client.post(`group_categories/${groupCategoryId}/groups`, data);
446
+ return response.data;
447
+ }
448
+ async getGroupsInCategory(groupCategoryId) {
449
+ return this.getAllPages(`group_categories/${groupCategoryId}/groups`);
450
+ }
451
+ async assignUnassignedMembers(groupCategoryId, sync = false) {
452
+ const response = await this.client.post(`group_categories/${groupCategoryId}/assign_unassigned_members`, { sync });
453
+ return response.data;
454
+ }
455
+ async addGroupMember(groupId, userId) {
456
+ const response = await this.client.post(`groups/${groupId}/memberships`, { user_id: userId });
457
+ return response.data;
458
+ }
459
+ }