@charlie.act7/canvas-mcp-server 1.1.8 → 1.2.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,357 @@
1
+ import { resolveCourseId } from "../common/helpers.js";
2
+ import { z } from "zod";
3
+ export const newQuizTools = [
4
+ {
5
+ name: "canvas_create_new_quiz",
6
+ tool: {
7
+ name: "canvas_create_new_quiz",
8
+ description: "Create a New Quiz (LTI) in a course. Use this for Canvas's modern quiz engine (not Classic Quizzes).",
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: {
12
+ course_id: {
13
+ anyOf: [{ type: "number" }, { type: "string" }],
14
+ description: "The ID or name of the course"
15
+ },
16
+ title: { type: "string", description: "Quiz title" },
17
+ instructions: { type: "string", description: "HTML instructions shown to students before starting" },
18
+ due_at: { type: "string", description: "Due date (ISO 8601)" },
19
+ lock_at: { type: "string", description: "Lock date (ISO 8601)" },
20
+ unlock_at: { type: "string", description: "Available from date (ISO 8601)" },
21
+ points_possible: { type: "number", description: "Total points possible" },
22
+ time_limit: { type: "number", description: "Time limit in minutes (null for no limit)" },
23
+ allowed_attempts: { type: "number", description: "Number of attempts allowed (-1 for unlimited)" },
24
+ shuffle_answers: { type: "boolean", description: "Randomize answer order" },
25
+ shuffle_questions: { type: "boolean", description: "Randomize question order" },
26
+ one_question_at_a_time: { type: "boolean", description: "Show one question per page" },
27
+ cant_go_back: { type: "boolean", description: "Prevent going back to previous questions" },
28
+ show_correct_answers: { type: "boolean", description: "Show correct answers after submission" }
29
+ },
30
+ required: ["course_id", "title"],
31
+ },
32
+ },
33
+ handler: async (client, args) => {
34
+ const input = z.object({
35
+ course_id: z.union([z.number(), z.string()]),
36
+ title: z.string(),
37
+ instructions: z.string().optional(),
38
+ due_at: z.string().optional(),
39
+ lock_at: z.string().optional(),
40
+ unlock_at: z.string().optional(),
41
+ points_possible: z.number().optional(),
42
+ time_limit: z.number().optional(),
43
+ allowed_attempts: z.number().optional(),
44
+ shuffle_answers: z.boolean().optional(),
45
+ shuffle_questions: z.boolean().optional(),
46
+ one_question_at_a_time: z.boolean().optional(),
47
+ cant_go_back: z.boolean().optional(),
48
+ show_correct_answers: z.boolean().optional()
49
+ }).parse(args);
50
+ const courseId = await resolveCourseId(client, input.course_id);
51
+ const { course_id: _, ...quizData } = input;
52
+ const result = await client.createNewQuiz(courseId, quizData);
53
+ return {
54
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
55
+ };
56
+ }
57
+ },
58
+ {
59
+ name: "canvas_update_new_quiz",
60
+ tool: {
61
+ name: "canvas_update_new_quiz",
62
+ description: "Update an existing New Quiz (LTI)",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ course_id: {
67
+ anyOf: [{ type: "number" }, { type: "string" }],
68
+ description: "The ID or name of the course"
69
+ },
70
+ quiz_id: { type: "string", description: "The New Quiz ID (assignment ID)" },
71
+ title: { type: "string" },
72
+ instructions: { type: "string" },
73
+ due_at: { type: "string", description: "ISO 8601 date or null to clear" },
74
+ lock_at: { type: "string" },
75
+ unlock_at: { type: "string" },
76
+ points_possible: { type: "number" },
77
+ time_limit: { type: "number", description: "Minutes (null to remove limit)" },
78
+ allowed_attempts: { type: "number" },
79
+ shuffle_answers: { type: "boolean" },
80
+ shuffle_questions: { type: "boolean" },
81
+ show_correct_answers: { type: "boolean" }
82
+ },
83
+ required: ["course_id", "quiz_id"],
84
+ },
85
+ },
86
+ handler: async (client, args) => {
87
+ const input = z.object({
88
+ course_id: z.union([z.number(), z.string()]),
89
+ quiz_id: z.string(),
90
+ title: z.string().optional(),
91
+ instructions: z.string().optional(),
92
+ due_at: z.string().optional(),
93
+ lock_at: z.string().optional(),
94
+ unlock_at: z.string().optional(),
95
+ points_possible: z.number().optional(),
96
+ time_limit: z.number().nullable().optional(),
97
+ allowed_attempts: z.number().optional(),
98
+ shuffle_answers: z.boolean().optional(),
99
+ shuffle_questions: z.boolean().optional(),
100
+ show_correct_answers: z.boolean().optional()
101
+ }).parse(args);
102
+ const courseId = await resolveCourseId(client, input.course_id);
103
+ const { course_id: _, quiz_id, ...updateData } = input;
104
+ const result = await client.updateNewQuiz(courseId, quiz_id, updateData);
105
+ return {
106
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
107
+ };
108
+ }
109
+ },
110
+ {
111
+ name: "canvas_delete_new_quiz",
112
+ tool: {
113
+ name: "canvas_delete_new_quiz",
114
+ description: "Delete a New Quiz (LTI) from a course",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ course_id: {
119
+ anyOf: [{ type: "number" }, { type: "string" }],
120
+ description: "The ID or name of the course"
121
+ },
122
+ quiz_id: { type: "string", description: "The New Quiz ID to delete" }
123
+ },
124
+ required: ["course_id", "quiz_id"],
125
+ },
126
+ },
127
+ handler: async (client, args) => {
128
+ const input = z.object({
129
+ course_id: z.union([z.number(), z.string()]),
130
+ quiz_id: z.string()
131
+ }).parse(args);
132
+ const courseId = await resolveCourseId(client, input.course_id);
133
+ const result = await client.deleteNewQuiz(courseId, input.quiz_id);
134
+ return {
135
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
136
+ };
137
+ }
138
+ },
139
+ {
140
+ name: "canvas_list_new_quiz_items",
141
+ tool: {
142
+ name: "canvas_list_new_quiz_items",
143
+ description: "List all questions/items in a New Quiz (LTI)",
144
+ inputSchema: {
145
+ type: "object",
146
+ properties: {
147
+ course_id: {
148
+ anyOf: [{ type: "number" }, { type: "string" }],
149
+ description: "The ID or name of the course"
150
+ },
151
+ quiz_id: { type: "string", description: "The New Quiz ID" }
152
+ },
153
+ required: ["course_id", "quiz_id"],
154
+ },
155
+ },
156
+ handler: async (client, args) => {
157
+ const input = z.object({
158
+ course_id: z.union([z.number(), z.string()]),
159
+ quiz_id: z.string()
160
+ }).parse(args);
161
+ const courseId = await resolveCourseId(client, input.course_id);
162
+ const result = await client.listNewQuizItems(courseId, input.quiz_id);
163
+ return {
164
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
165
+ };
166
+ }
167
+ },
168
+ {
169
+ name: "canvas_get_new_quiz_item",
170
+ tool: {
171
+ name: "canvas_get_new_quiz_item",
172
+ description: "Get details of a specific item/question in a New Quiz (LTI)",
173
+ inputSchema: {
174
+ type: "object",
175
+ properties: {
176
+ course_id: {
177
+ anyOf: [{ type: "number" }, { type: "string" }],
178
+ description: "The ID or name of the course"
179
+ },
180
+ quiz_id: { type: "string", description: "The New Quiz ID" },
181
+ item_id: { type: "string", description: "The item ID" }
182
+ },
183
+ required: ["course_id", "quiz_id", "item_id"],
184
+ },
185
+ },
186
+ handler: async (client, args) => {
187
+ const input = z.object({
188
+ course_id: z.union([z.number(), z.string()]),
189
+ quiz_id: z.string(),
190
+ item_id: z.string()
191
+ }).parse(args);
192
+ const courseId = await resolveCourseId(client, input.course_id);
193
+ const result = await client.getNewQuizItem(courseId, input.quiz_id, input.item_id);
194
+ return {
195
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
196
+ };
197
+ }
198
+ },
199
+ {
200
+ name: "canvas_create_new_quiz_item",
201
+ tool: {
202
+ name: "canvas_create_new_quiz_item",
203
+ description: "Add a question/item to a New Quiz (LTI). Supports multiple choice, true/false, essay, fill-in-blank, matching, and more.",
204
+ inputSchema: {
205
+ type: "object",
206
+ properties: {
207
+ course_id: {
208
+ anyOf: [{ type: "number" }, { type: "string" }],
209
+ description: "The ID or name of the course"
210
+ },
211
+ quiz_id: { type: "string", description: "The New Quiz ID" },
212
+ points_possible: { type: "number", description: "Points for this item (default: 1)" },
213
+ position: { type: "number", description: "Position order in the quiz" },
214
+ entry_type: {
215
+ type: "string",
216
+ description: "Always use 'Item' for a standard question"
217
+ },
218
+ title: { type: "string", description: "Question title/name" },
219
+ item_body: { type: "string", description: "HTML question text shown to students" },
220
+ interaction_type_slug: {
221
+ type: "string",
222
+ description: "Question type: choice (multiple choice), true-false, essay, short-answer, matching, ordering, file-upload, categorization, hot-spot"
223
+ },
224
+ interaction_data: {
225
+ type: "object",
226
+ description: "Question-type-specific data (choices, correct answers, etc.)"
227
+ },
228
+ scoring_data: {
229
+ type: "object",
230
+ description: "Scoring configuration for the question"
231
+ }
232
+ },
233
+ required: ["course_id", "quiz_id", "entry_type", "item_body", "interaction_type_slug"],
234
+ },
235
+ },
236
+ handler: async (client, args) => {
237
+ const input = z.object({
238
+ course_id: z.union([z.number(), z.string()]),
239
+ quiz_id: z.string(),
240
+ points_possible: z.number().optional(),
241
+ position: z.number().optional(),
242
+ entry_type: z.string(),
243
+ title: z.string().optional(),
244
+ item_body: z.string(),
245
+ interaction_type_slug: z.string(),
246
+ interaction_data: z.record(z.any()).optional(),
247
+ scoring_data: z.record(z.any()).optional()
248
+ }).parse(args);
249
+ const courseId = await resolveCourseId(client, input.course_id);
250
+ const result = await client.createNewQuizItem(courseId, input.quiz_id, {
251
+ position: input.position,
252
+ points_possible: input.points_possible,
253
+ entry_type: input.entry_type,
254
+ entry: {
255
+ title: input.title,
256
+ item_body: input.item_body,
257
+ interaction_type_slug: input.interaction_type_slug,
258
+ interaction_data: input.interaction_data,
259
+ scoring_data: input.scoring_data
260
+ }
261
+ });
262
+ return {
263
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
264
+ };
265
+ }
266
+ },
267
+ {
268
+ name: "canvas_update_new_quiz_item",
269
+ tool: {
270
+ name: "canvas_update_new_quiz_item",
271
+ description: "Update an existing question/item in a New Quiz (LTI)",
272
+ inputSchema: {
273
+ type: "object",
274
+ properties: {
275
+ course_id: {
276
+ anyOf: [{ type: "number" }, { type: "string" }],
277
+ description: "The ID or name of the course"
278
+ },
279
+ quiz_id: { type: "string", description: "The New Quiz ID" },
280
+ item_id: { type: "string", description: "The item ID to update" },
281
+ points_possible: { type: "number" },
282
+ position: { type: "number" },
283
+ title: { type: "string" },
284
+ item_body: { type: "string", description: "HTML question text" },
285
+ interaction_data: { type: "object" },
286
+ scoring_data: { type: "object" }
287
+ },
288
+ required: ["course_id", "quiz_id", "item_id"],
289
+ },
290
+ },
291
+ handler: async (client, args) => {
292
+ const input = z.object({
293
+ course_id: z.union([z.number(), z.string()]),
294
+ quiz_id: z.string(),
295
+ item_id: z.string(),
296
+ points_possible: z.number().optional(),
297
+ position: z.number().optional(),
298
+ title: z.string().optional(),
299
+ item_body: z.string().optional(),
300
+ interaction_data: z.record(z.any()).optional(),
301
+ scoring_data: z.record(z.any()).optional()
302
+ }).parse(args);
303
+ const courseId = await resolveCourseId(client, input.course_id);
304
+ const updatePayload = {};
305
+ if (input.points_possible !== undefined)
306
+ updatePayload.points_possible = input.points_possible;
307
+ if (input.position !== undefined)
308
+ updatePayload.position = input.position;
309
+ if (input.title || input.item_body || input.interaction_data || input.scoring_data) {
310
+ updatePayload.entry = {};
311
+ if (input.title)
312
+ updatePayload.entry.title = input.title;
313
+ if (input.item_body)
314
+ updatePayload.entry.item_body = input.item_body;
315
+ if (input.interaction_data)
316
+ updatePayload.entry.interaction_data = input.interaction_data;
317
+ if (input.scoring_data)
318
+ updatePayload.entry.scoring_data = input.scoring_data;
319
+ }
320
+ const result = await client.updateNewQuizItem(courseId, input.quiz_id, input.item_id, updatePayload);
321
+ return {
322
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
323
+ };
324
+ }
325
+ },
326
+ {
327
+ name: "canvas_delete_new_quiz_item",
328
+ tool: {
329
+ name: "canvas_delete_new_quiz_item",
330
+ description: "Delete a question/item from a New Quiz (LTI)",
331
+ inputSchema: {
332
+ type: "object",
333
+ properties: {
334
+ course_id: {
335
+ anyOf: [{ type: "number" }, { type: "string" }],
336
+ description: "The ID or name of the course"
337
+ },
338
+ quiz_id: { type: "string", description: "The New Quiz ID" },
339
+ item_id: { type: "string", description: "The item ID to delete" }
340
+ },
341
+ required: ["course_id", "quiz_id", "item_id"],
342
+ },
343
+ },
344
+ handler: async (client, args) => {
345
+ const input = z.object({
346
+ course_id: z.union([z.number(), z.string()]),
347
+ quiz_id: z.string(),
348
+ item_id: z.string()
349
+ }).parse(args);
350
+ const courseId = await resolveCourseId(client, input.course_id);
351
+ const result = await client.deleteNewQuizItem(courseId, input.quiz_id, input.item_id);
352
+ return {
353
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
354
+ };
355
+ }
356
+ }
357
+ ];
@@ -0,0 +1,130 @@
1
+ import { resolveCourseId } from "../common/helpers.js";
2
+ import { z } from "zod";
3
+ export const peerReviewTools = [
4
+ {
5
+ name: "canvas_list_peer_reviews",
6
+ tool: {
7
+ name: "canvas_list_peer_reviews",
8
+ description: "List all peer reviews assigned for an assignment",
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: {
12
+ course_id: {
13
+ anyOf: [{ type: "number" }, { type: "string" }],
14
+ description: "The ID or name of the course"
15
+ },
16
+ assignment_id: { type: "number", description: "The ID of the assignment" }
17
+ },
18
+ required: ["course_id", "assignment_id"],
19
+ },
20
+ },
21
+ handler: async (client, args) => {
22
+ const input = z.object({
23
+ course_id: z.union([z.number(), z.string()]),
24
+ assignment_id: z.coerce.number()
25
+ }).parse(args);
26
+ const courseId = await resolveCourseId(client, input.course_id);
27
+ const result = await client.listPeerReviews(courseId, input.assignment_id);
28
+ return {
29
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
30
+ };
31
+ }
32
+ },
33
+ {
34
+ name: "canvas_get_submission_peer_reviews",
35
+ tool: {
36
+ name: "canvas_get_submission_peer_reviews",
37
+ description: "List peer reviews assigned to a specific student submission",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ course_id: {
42
+ anyOf: [{ type: "number" }, { type: "string" }],
43
+ description: "The ID or name of the course"
44
+ },
45
+ assignment_id: { type: "number", description: "The ID of the assignment" },
46
+ student_id: { type: "number", description: "The Canvas user ID of the student whose submission is being reviewed" }
47
+ },
48
+ required: ["course_id", "assignment_id", "student_id"],
49
+ },
50
+ },
51
+ handler: async (client, args) => {
52
+ const input = z.object({
53
+ course_id: z.union([z.number(), z.string()]),
54
+ assignment_id: z.coerce.number(),
55
+ student_id: z.coerce.number()
56
+ }).parse(args);
57
+ const courseId = await resolveCourseId(client, input.course_id);
58
+ const result = await client.getSubmissionPeerReviews(courseId, input.assignment_id, input.student_id);
59
+ return {
60
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
61
+ };
62
+ }
63
+ },
64
+ {
65
+ name: "canvas_create_peer_review",
66
+ tool: {
67
+ name: "canvas_create_peer_review",
68
+ description: "Manually assign a peer review: one student reviews another student's submission",
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {
72
+ course_id: {
73
+ anyOf: [{ type: "number" }, { type: "string" }],
74
+ description: "The ID or name of the course"
75
+ },
76
+ assignment_id: { type: "number", description: "The ID of the assignment" },
77
+ submission_student_id: { type: "number", description: "User ID of the student whose submission will be reviewed" },
78
+ reviewer_id: { type: "number", description: "User ID of the student who will perform the review" }
79
+ },
80
+ required: ["course_id", "assignment_id", "submission_student_id", "reviewer_id"],
81
+ },
82
+ },
83
+ handler: async (client, args) => {
84
+ const input = z.object({
85
+ course_id: z.union([z.number(), z.string()]),
86
+ assignment_id: z.coerce.number(),
87
+ submission_student_id: z.coerce.number(),
88
+ reviewer_id: z.coerce.number()
89
+ }).parse(args);
90
+ const courseId = await resolveCourseId(client, input.course_id);
91
+ const result = await client.createPeerReview(courseId, input.assignment_id, input.submission_student_id, input.reviewer_id);
92
+ return {
93
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
94
+ };
95
+ }
96
+ },
97
+ {
98
+ name: "canvas_delete_peer_review",
99
+ tool: {
100
+ name: "canvas_delete_peer_review",
101
+ description: "Remove a peer review assignment",
102
+ inputSchema: {
103
+ type: "object",
104
+ properties: {
105
+ course_id: {
106
+ anyOf: [{ type: "number" }, { type: "string" }],
107
+ description: "The ID or name of the course"
108
+ },
109
+ assignment_id: { type: "number", description: "The ID of the assignment" },
110
+ submission_student_id: { type: "number", description: "User ID of the student whose submission was being reviewed" },
111
+ reviewer_id: { type: "number", description: "User ID of the reviewer to remove" }
112
+ },
113
+ required: ["course_id", "assignment_id", "submission_student_id", "reviewer_id"],
114
+ },
115
+ },
116
+ handler: async (client, args) => {
117
+ const input = z.object({
118
+ course_id: z.union([z.number(), z.string()]),
119
+ assignment_id: z.coerce.number(),
120
+ submission_student_id: z.coerce.number(),
121
+ reviewer_id: z.coerce.number()
122
+ }).parse(args);
123
+ const courseId = await resolveCourseId(client, input.course_id);
124
+ const result = await client.deletePeerReview(courseId, input.assignment_id, input.submission_student_id, input.reviewer_id);
125
+ return {
126
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
127
+ };
128
+ }
129
+ }
130
+ ];