@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,303 @@
1
+ import { resolveCourseId } from "../common/helpers.js";
2
+ import { z } from "zod";
3
+ export const quizQuestionTools = [
4
+ {
5
+ name: "canvas_list_quiz_questions",
6
+ tool: {
7
+ name: "canvas_list_quiz_questions",
8
+ description: "List all questions in a quiz",
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
+ quiz_id: {
17
+ type: "number",
18
+ description: "The quiz ID"
19
+ }
20
+ },
21
+ required: ["course_id", "quiz_id"]
22
+ }
23
+ },
24
+ handler: async (client, args) => {
25
+ const input = z.object({
26
+ course_id: z.union([z.number(), z.string()]),
27
+ quiz_id: z.coerce.number()
28
+ }).parse(args);
29
+ const courseId = await resolveCourseId(client, input.course_id);
30
+ const questions = await client.listQuizQuestions(courseId, input.quiz_id);
31
+ return {
32
+ content: [{ type: "text", text: JSON.stringify(questions, null, 2) }]
33
+ };
34
+ }
35
+ },
36
+ {
37
+ name: "canvas_create_quiz_question",
38
+ tool: {
39
+ name: "canvas_create_quiz_question",
40
+ description: "Create a question in a quiz. Supports types: multiple_choice_question, true_false_question, essay_question, short_answer_question, fill_in_multiple_blanks_question, multiple_answers_question, matching_question, numerical_question. Use quiz_group_id to assign the question to a group for random selection.",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {
44
+ course_id: {
45
+ anyOf: [{ type: "number" }, { type: "string" }],
46
+ description: "The ID or name of the course"
47
+ },
48
+ quiz_id: {
49
+ type: "number",
50
+ description: "The quiz ID"
51
+ },
52
+ question_name: {
53
+ type: "string",
54
+ description: "Short name/title for the question"
55
+ },
56
+ question_type: {
57
+ type: "string",
58
+ description: "Question type (e.g. multiple_choice_question, true_false_question, essay_question)"
59
+ },
60
+ question_text: {
61
+ type: "string",
62
+ description: "The question text (HTML supported)"
63
+ },
64
+ points_possible: {
65
+ type: "number",
66
+ description: "Point value for the question"
67
+ },
68
+ quiz_group_id: {
69
+ type: "number",
70
+ description: "Optional quiz group ID to assign this question to (for random picking)"
71
+ },
72
+ answers: {
73
+ type: "array",
74
+ description: "Array of answer objects. For multiple_choice: weight=100 for correct, weight=0 for incorrect.",
75
+ items: {
76
+ type: "object",
77
+ properties: {
78
+ text: { type: "string", description: "Answer text" },
79
+ weight: { type: "number", description: "100 for correct, 0 for incorrect" },
80
+ comments: { type: "string", description: "Optional feedback comment" }
81
+ },
82
+ required: ["text", "weight"]
83
+ }
84
+ }
85
+ },
86
+ required: ["course_id", "quiz_id", "question_name", "question_type", "question_text", "points_possible"]
87
+ }
88
+ },
89
+ handler: async (client, args) => {
90
+ const answerSchema = z.object({
91
+ text: z.string(),
92
+ blank_id: z.string().optional(),
93
+ weight: z.number(),
94
+ comments: z.string().optional()
95
+ });
96
+ const input = z.object({
97
+ course_id: z.union([z.number(), z.string()]),
98
+ quiz_id: z.coerce.number(),
99
+ question_name: z.string().min(1),
100
+ question_type: z.string().min(1),
101
+ question_text: z.string().min(1),
102
+ points_possible: z.coerce.number(),
103
+ quiz_group_id: z.coerce.number().optional(),
104
+ answers: z.array(answerSchema).optional()
105
+ }).parse(args);
106
+ const courseId = await resolveCourseId(client, input.course_id);
107
+ const question = await client.createQuizQuestion(courseId, input.quiz_id, {
108
+ question_name: input.question_name,
109
+ question_type: input.question_type,
110
+ question_text: input.question_text,
111
+ points_possible: input.points_possible,
112
+ quiz_group_id: input.quiz_group_id,
113
+ answers: input.answers
114
+ });
115
+ return {
116
+ content: [{ type: "text", text: JSON.stringify(question, null, 2) }]
117
+ };
118
+ }
119
+ },
120
+ {
121
+ name: "canvas_update_quiz_question",
122
+ tool: {
123
+ name: "canvas_update_quiz_question",
124
+ description: "Update an existing question in a quiz",
125
+ inputSchema: {
126
+ type: "object",
127
+ properties: {
128
+ course_id: {
129
+ anyOf: [{ type: "number" }, { type: "string" }],
130
+ description: "The ID or name of the course"
131
+ },
132
+ quiz_id: {
133
+ type: "number",
134
+ description: "The quiz ID"
135
+ },
136
+ question_id: {
137
+ type: "number",
138
+ description: "The question ID to update"
139
+ },
140
+ question_name: {
141
+ type: "string",
142
+ description: "New name/title for the question"
143
+ },
144
+ question_type: {
145
+ type: "string",
146
+ description: "New question type"
147
+ },
148
+ question_text: {
149
+ type: "string",
150
+ description: "New question text (HTML supported)"
151
+ },
152
+ points_possible: {
153
+ type: "number",
154
+ description: "New point value"
155
+ },
156
+ quiz_group_id: {
157
+ anyOf: [{ type: "number" }, { type: "null" }],
158
+ description: "Quiz group ID to assign to, or null to remove from group"
159
+ },
160
+ answers: {
161
+ type: "array",
162
+ description: "New answers array (replaces existing answers)",
163
+ items: {
164
+ type: "object",
165
+ properties: {
166
+ text: { type: "string" },
167
+ weight: { type: "number" },
168
+ comments: { type: "string" }
169
+ },
170
+ required: ["text", "weight"]
171
+ }
172
+ }
173
+ },
174
+ required: ["course_id", "quiz_id", "question_id"]
175
+ }
176
+ },
177
+ handler: async (client, args) => {
178
+ const answerSchema = z.object({
179
+ text: z.string(),
180
+ blank_id: z.string().optional(),
181
+ weight: z.number(),
182
+ comments: z.string().optional()
183
+ });
184
+ const input = z.object({
185
+ course_id: z.union([z.number(), z.string()]),
186
+ quiz_id: z.coerce.number(),
187
+ question_id: z.coerce.number(),
188
+ question_name: z.string().min(1).optional(),
189
+ question_type: z.string().min(1).optional(),
190
+ question_text: z.string().min(1).optional(),
191
+ points_possible: z.coerce.number().optional(),
192
+ quiz_group_id: z.coerce.number().nullable().optional(),
193
+ answers: z.array(answerSchema).optional()
194
+ }).parse(args);
195
+ const courseId = await resolveCourseId(client, input.course_id);
196
+ const data = {};
197
+ if (input.question_name !== undefined)
198
+ data.question_name = input.question_name;
199
+ if (input.question_type !== undefined)
200
+ data.question_type = input.question_type;
201
+ if (input.question_text !== undefined)
202
+ data.question_text = input.question_text;
203
+ if (input.points_possible !== undefined)
204
+ data.points_possible = input.points_possible;
205
+ if (input.quiz_group_id !== undefined)
206
+ data.quiz_group_id = input.quiz_group_id;
207
+ if (input.answers !== undefined)
208
+ data.answers = input.answers;
209
+ const question = await client.updateQuizQuestion(courseId, input.quiz_id, input.question_id, data);
210
+ return {
211
+ content: [{ type: "text", text: JSON.stringify(question, null, 2) }]
212
+ };
213
+ }
214
+ },
215
+ {
216
+ name: "canvas_delete_quiz_question",
217
+ tool: {
218
+ name: "canvas_delete_quiz_question",
219
+ description: "Delete a question from a quiz",
220
+ inputSchema: {
221
+ type: "object",
222
+ properties: {
223
+ course_id: {
224
+ anyOf: [{ type: "number" }, { type: "string" }],
225
+ description: "The ID or name of the course"
226
+ },
227
+ quiz_id: {
228
+ type: "number",
229
+ description: "The quiz ID"
230
+ },
231
+ question_id: {
232
+ type: "number",
233
+ description: "The question ID to delete"
234
+ }
235
+ },
236
+ required: ["course_id", "quiz_id", "question_id"]
237
+ }
238
+ },
239
+ handler: async (client, args) => {
240
+ const input = z.object({
241
+ course_id: z.union([z.number(), z.string()]),
242
+ quiz_id: z.coerce.number(),
243
+ question_id: z.coerce.number()
244
+ }).parse(args);
245
+ const courseId = await resolveCourseId(client, input.course_id);
246
+ const result = await client.deleteQuizQuestion(courseId, input.quiz_id, input.question_id);
247
+ return {
248
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
249
+ };
250
+ }
251
+ },
252
+ {
253
+ name: "canvas_create_quiz_group",
254
+ tool: {
255
+ name: "canvas_create_quiz_group",
256
+ description: "Create a quiz group in a quiz. Questions assigned to the group via quiz_group_id will be randomly picked (pick_count) when students take the quiz.",
257
+ inputSchema: {
258
+ type: "object",
259
+ properties: {
260
+ course_id: {
261
+ anyOf: [{ type: "number" }, { type: "string" }],
262
+ description: "The ID or name of the course"
263
+ },
264
+ quiz_id: {
265
+ type: "number",
266
+ description: "The quiz ID"
267
+ },
268
+ name: {
269
+ type: "string",
270
+ description: "Name for the quiz group"
271
+ },
272
+ pick_count: {
273
+ type: "number",
274
+ description: "Number of questions to randomly pick from this group"
275
+ },
276
+ question_points: {
277
+ type: "number",
278
+ description: "Points per question in this group"
279
+ }
280
+ },
281
+ required: ["course_id", "quiz_id", "name", "pick_count", "question_points"]
282
+ }
283
+ },
284
+ handler: async (client, args) => {
285
+ const input = z.object({
286
+ course_id: z.union([z.number(), z.string()]),
287
+ quiz_id: z.coerce.number(),
288
+ name: z.string().min(1),
289
+ pick_count: z.coerce.number().min(1),
290
+ question_points: z.coerce.number().min(0)
291
+ }).parse(args);
292
+ const courseId = await resolveCourseId(client, input.course_id);
293
+ const group = await client.createQuizGroup(courseId, input.quiz_id, {
294
+ name: input.name,
295
+ pick_count: input.pick_count,
296
+ question_points: input.question_points
297
+ });
298
+ return {
299
+ content: [{ type: "text", text: JSON.stringify(group, null, 2) }]
300
+ };
301
+ }
302
+ }
303
+ ];
@@ -0,0 +1,184 @@
1
+ import { resolveCourseId } from "../common/helpers.js";
2
+ import { z } from "zod";
3
+ export const quizTools = [
4
+ {
5
+ name: "canvas_list_quizzes",
6
+ tool: {
7
+ name: "canvas_list_quizzes",
8
+ description: "List quizzes for a specific course",
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
+ },
17
+ required: ["course_id"]
18
+ }
19
+ },
20
+ handler: async (client, args) => {
21
+ const input = z.object({
22
+ course_id: z.union([z.number(), z.string()])
23
+ }).parse(args);
24
+ const courseId = await resolveCourseId(client, input.course_id);
25
+ const quizzes = await client.getQuizzes(courseId);
26
+ return {
27
+ content: [{ type: "text", text: JSON.stringify(quizzes, null, 2) }]
28
+ };
29
+ }
30
+ },
31
+ {
32
+ name: "canvas_get_quiz",
33
+ tool: {
34
+ name: "canvas_get_quiz",
35
+ description: "Get details for a specific quiz",
36
+ inputSchema: {
37
+ type: "object",
38
+ properties: {
39
+ course_id: {
40
+ anyOf: [{ type: "number" }, { type: "string" }],
41
+ description: "The ID or name of the course"
42
+ },
43
+ quiz_id: { type: "number", description: "The quiz ID" }
44
+ },
45
+ required: ["course_id", "quiz_id"]
46
+ }
47
+ },
48
+ handler: async (client, args) => {
49
+ const input = z.object({
50
+ course_id: z.union([z.number(), z.string()]),
51
+ quiz_id: z.number()
52
+ }).parse(args);
53
+ const courseId = await resolveCourseId(client, input.course_id);
54
+ const quiz = await client.getQuiz(courseId, input.quiz_id);
55
+ return {
56
+ content: [{ type: "text", text: JSON.stringify(quiz, null, 2) }]
57
+ };
58
+ }
59
+ },
60
+ {
61
+ name: "canvas_update_quiz_dates",
62
+ tool: {
63
+ name: "canvas_update_quiz_dates",
64
+ description: "Update due/unlock/lock dates for a specific quiz",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ course_id: {
69
+ anyOf: [{ type: "number" }, { type: "string" }],
70
+ description: "The ID or name of the course"
71
+ },
72
+ quiz_id: { type: "number", description: "The quiz ID" },
73
+ due_at: { type: "string", description: "ISO-8601 due date. Use null to clear.", nullable: true },
74
+ unlock_at: { type: "string", description: "ISO-8601 unlock date. Use null to clear.", nullable: true },
75
+ lock_at: { type: "string", description: "ISO-8601 lock date. Use null to clear.", nullable: true }
76
+ },
77
+ required: ["course_id", "quiz_id"]
78
+ }
79
+ },
80
+ handler: async (client, args) => {
81
+ const input = z.object({
82
+ course_id: z.union([z.number(), z.string()]),
83
+ quiz_id: z.number(),
84
+ due_at: z.string().nullable().optional(),
85
+ unlock_at: z.string().nullable().optional(),
86
+ lock_at: z.string().nullable().optional()
87
+ }).parse(args);
88
+ const courseId = await resolveCourseId(client, input.course_id);
89
+ const quiz = await client.updateQuiz(courseId, input.quiz_id, {
90
+ due_at: input.due_at,
91
+ unlock_at: input.unlock_at,
92
+ lock_at: input.lock_at
93
+ });
94
+ return {
95
+ content: [{ type: "text", text: JSON.stringify(quiz, null, 2) }]
96
+ };
97
+ }
98
+ },
99
+ {
100
+ name: "canvas_create_quiz",
101
+ tool: {
102
+ name: "canvas_create_quiz",
103
+ description: "Create a new quiz for a course",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ course_id: { anyOf: [{ type: "number" }, { type: "string" }] },
108
+ title: { type: "string" },
109
+ description: { type: "string" },
110
+ quiz_type: { type: "string", enum: ["practice_quiz", "assignment", "graded_survey", "survey"] },
111
+ time_limit: { type: "number" },
112
+ shuffle_answers: { type: "boolean" },
113
+ published: { type: "boolean" },
114
+ assignment_group_id: { type: "number" },
115
+ allowed_attempts: { type: "number" },
116
+ require_lockdown_browser: { type: "boolean" },
117
+ show_correct_answers: { type: "boolean" },
118
+ show_correct_answers_at: { type: "string", description: "ISO-8601 date to show correct answers" }
119
+ },
120
+ required: ["course_id", "title"]
121
+ }
122
+ },
123
+ handler: async (client, args) => {
124
+ const input = z.object({
125
+ course_id: z.union([z.number(), z.string()]),
126
+ title: z.string(),
127
+ description: z.string().optional(),
128
+ quiz_type: z.enum(["practice_quiz", "assignment", "graded_survey", "survey"]).optional(),
129
+ time_limit: z.number().optional(),
130
+ shuffle_answers: z.boolean().optional(),
131
+ published: z.boolean().optional(),
132
+ assignment_group_id: z.number().optional(),
133
+ allowed_attempts: z.number().optional(),
134
+ require_lockdown_browser: z.boolean().optional(),
135
+ show_correct_answers: z.boolean().optional(),
136
+ show_correct_answers_at: z.string().optional()
137
+ }).parse(args);
138
+ const courseId = await resolveCourseId(client, input.course_id);
139
+ const quizData = { ...input };
140
+ delete quizData.course_id;
141
+ const quiz = await client.createQuiz(courseId, quizData);
142
+ return { content: [{ type: "text", text: JSON.stringify(quiz, null, 2) }] };
143
+ }
144
+ },
145
+ {
146
+ name: "canvas_update_quiz",
147
+ tool: {
148
+ name: "canvas_update_quiz",
149
+ description: "Update an existing quiz",
150
+ inputSchema: {
151
+ type: "object",
152
+ properties: {
153
+ course_id: { anyOf: [{ type: "number" }, { type: "string" }] },
154
+ quiz_id: { type: "number" },
155
+ title: { type: "string" },
156
+ description: { type: "string" },
157
+ published: { type: "boolean" },
158
+ require_lockdown_browser: { type: "boolean" },
159
+ show_correct_answers: { type: "boolean" },
160
+ show_correct_answers_at: { type: "string", description: "ISO-8601 date to show correct answers" }
161
+ },
162
+ required: ["course_id", "quiz_id"]
163
+ }
164
+ },
165
+ handler: async (client, args) => {
166
+ const input = z.object({
167
+ course_id: z.union([z.number(), z.string()]),
168
+ quiz_id: z.number(),
169
+ title: z.string().optional(),
170
+ description: z.string().optional(),
171
+ published: z.boolean().optional(),
172
+ require_lockdown_browser: z.boolean().optional(),
173
+ show_correct_answers: z.boolean().optional(),
174
+ show_correct_answers_at: z.string().optional()
175
+ }).parse(args);
176
+ const courseId = await resolveCourseId(client, input.course_id);
177
+ const quizData = { ...input };
178
+ delete quizData.course_id;
179
+ delete quizData.quiz_id;
180
+ const quiz = await client.updateQuiz(courseId, input.quiz_id, quizData);
181
+ return { content: [{ type: "text", text: JSON.stringify(quiz, null, 2) }] };
182
+ }
183
+ }
184
+ ];
@@ -0,0 +1,145 @@
1
+ import { resolveCourseId } from "../common/helpers.js";
2
+ import { z } from "zod";
3
+ export const rubricTools = [
4
+ {
5
+ name: "canvas_create_rubric",
6
+ tool: {
7
+ name: "canvas_create_rubric",
8
+ description: "Create a new rubric in a course and optionally associate it with 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
+ title: { type: "string", description: "The title of the rubric" },
17
+ criteria: {
18
+ type: "array",
19
+ description: "List of criteria objects for the rubric",
20
+ items: {
21
+ type: "object",
22
+ properties: {
23
+ description: { type: "string" },
24
+ long_description: { type: "string" },
25
+ points: { type: "number" },
26
+ ratings: {
27
+ type: "array",
28
+ items: {
29
+ type: "object",
30
+ properties: {
31
+ description: { type: "string" },
32
+ points: { type: "number" },
33
+ long_description: { type: "string" }
34
+ },
35
+ required: ["description", "points"]
36
+ }
37
+ }
38
+ },
39
+ required: ["description", "points", "ratings"]
40
+ }
41
+ },
42
+ association_id: { type: "number", description: "Optional Assignment ID to associate the rubric with" },
43
+ association_type: { type: "string", description: "Optional Type of association, defaults to 'Assignment'" },
44
+ use_for_grading: { type: "boolean", description: "Optional Whether to use the rubric for grading" },
45
+ purpose: { type: "string", description: "Optional Purpose of association, e.g. 'grading'" }
46
+ },
47
+ required: ["course_id", "title", "criteria"],
48
+ },
49
+ },
50
+ handler: async (client, args) => {
51
+ const input = z.object({
52
+ course_id: z.union([z.number(), z.string()]),
53
+ title: z.string(),
54
+ criteria: z.array(z.object({
55
+ description: z.string(),
56
+ long_description: z.string().optional(),
57
+ points: z.number(),
58
+ ratings: z.array(z.object({
59
+ description: z.string(),
60
+ points: z.number(),
61
+ long_description: z.string().optional()
62
+ }))
63
+ })),
64
+ association_id: z.number().optional(),
65
+ association_type: z.string().optional().default("Assignment"),
66
+ use_for_grading: z.boolean().optional(),
67
+ purpose: z.string().optional().default("grading")
68
+ }).parse(args);
69
+ const courseId = await resolveCourseId(client, input.course_id);
70
+ // Format criteria as a dictionary keyed by index as Canvas API requires indexed hash/array
71
+ const criteriaRecord = {};
72
+ input.criteria.forEach((c, index) => {
73
+ const ratingsRecord = {};
74
+ c.ratings.forEach((r, rIndex) => {
75
+ ratingsRecord[rIndex.toString()] = r;
76
+ });
77
+ criteriaRecord[index.toString()] = {
78
+ ...c,
79
+ ratings: ratingsRecord
80
+ };
81
+ });
82
+ const rubricData = {
83
+ title: input.title,
84
+ criteria: criteriaRecord
85
+ };
86
+ let associationData = undefined;
87
+ if (input.association_id) {
88
+ associationData = {
89
+ association_id: input.association_id,
90
+ association_type: input.association_type,
91
+ use_for_grading: input.use_for_grading,
92
+ purpose: input.purpose
93
+ };
94
+ }
95
+ const rubric = await client.createRubric(courseId, rubricData, associationData);
96
+ return {
97
+ content: [{ type: "text", text: JSON.stringify(rubric, null, 2) }],
98
+ };
99
+ }
100
+ },
101
+ {
102
+ name: "canvas_create_rubric_association",
103
+ tool: {
104
+ name: "canvas_create_rubric_association",
105
+ description: "Associate an existing rubric with an object like an Assignment",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ course_id: {
110
+ anyOf: [{ type: "number" }, { type: "string" }],
111
+ description: "The ID or name of the course"
112
+ },
113
+ rubric_id: { type: "number", description: "The ID of the rubric to associate" },
114
+ association_id: { type: "number", description: "The ID of the object (e.g., Assignment ID)" },
115
+ association_type: { type: "string", description: "Type of object, defaults to 'Assignment'" },
116
+ use_for_grading: { type: "boolean", description: "Whether to use the rubric for grading" },
117
+ purpose: { type: "string", description: "Purpose of association, e.g. 'grading'" }
118
+ },
119
+ required: ["course_id", "rubric_id", "association_id"],
120
+ },
121
+ },
122
+ handler: async (client, args) => {
123
+ const input = z.object({
124
+ course_id: z.union([z.number(), z.string()]),
125
+ rubric_id: z.number(),
126
+ association_id: z.number(),
127
+ association_type: z.string().optional().default("Assignment"),
128
+ use_for_grading: z.boolean().optional(),
129
+ purpose: z.string().optional().default("grading")
130
+ }).parse(args);
131
+ const courseId = await resolveCourseId(client, input.course_id);
132
+ const associationData = {
133
+ rubric_id: input.rubric_id,
134
+ association_id: input.association_id,
135
+ association_type: input.association_type,
136
+ use_for_grading: input.use_for_grading,
137
+ purpose: input.purpose
138
+ };
139
+ const association = await client.createRubricAssociation(courseId, associationData);
140
+ return {
141
+ content: [{ type: "text", text: JSON.stringify(association, null, 2) }],
142
+ };
143
+ }
144
+ }
145
+ ];