@charlie.act7/canvas-mcp-server 1.2.0 → 1.2.1
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.
- package/dist/index.js +14 -4
- package/dist/services/canvas-client.js +79 -6
- package/dist/tools/module-tools.js +50 -7
- package/dist/tools/question-bank-tools.js +238 -0
- package/dist/tools/quiz-tools.js +72 -0
- package/package.json +76 -70
- package/.claude-plugin/plugin.json +0 -11
- package/.mcp.json +0 -12
package/dist/index.js
CHANGED
|
@@ -35,6 +35,16 @@ import { startHttpServer } from "./http-server.js";
|
|
|
35
35
|
dotenv.config();
|
|
36
36
|
const configManager = new ConfigManager();
|
|
37
37
|
const program = new Command();
|
|
38
|
+
function uniqueTools(tools) {
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
return tools.filter((tool) => {
|
|
41
|
+
if (seen.has(tool.name)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
seen.add(tool.name);
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
38
48
|
program
|
|
39
49
|
.name("canvas-mcp")
|
|
40
50
|
.description("MCP Server for Canvas LMS (Refactored)")
|
|
@@ -96,7 +106,7 @@ program
|
|
|
96
106
|
},
|
|
97
107
|
});
|
|
98
108
|
// --- Aggregation ---
|
|
99
|
-
const allTools = [
|
|
109
|
+
const allTools = uniqueTools([
|
|
100
110
|
...courseTools,
|
|
101
111
|
...assignmentTools,
|
|
102
112
|
...quizTools,
|
|
@@ -116,7 +126,7 @@ program
|
|
|
116
126
|
...newQuizTools,
|
|
117
127
|
...analyticsTools,
|
|
118
128
|
...peerReviewTools
|
|
119
|
-
];
|
|
129
|
+
]);
|
|
120
130
|
// --- Tool Handlers ---
|
|
121
131
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
122
132
|
return {
|
|
@@ -173,7 +183,7 @@ program
|
|
|
173
183
|
.action(async (options) => {
|
|
174
184
|
const client = getClient();
|
|
175
185
|
const port = Number.parseInt(options.port, 10);
|
|
176
|
-
const allTools = [
|
|
186
|
+
const allTools = uniqueTools([
|
|
177
187
|
...courseTools,
|
|
178
188
|
...assignmentTools,
|
|
179
189
|
...quizTools,
|
|
@@ -193,7 +203,7 @@ program
|
|
|
193
203
|
...newQuizTools,
|
|
194
204
|
...analyticsTools,
|
|
195
205
|
...peerReviewTools
|
|
196
|
-
];
|
|
206
|
+
]);
|
|
197
207
|
await startHttpServer(client, options.host, port, allTools);
|
|
198
208
|
});
|
|
199
209
|
program.parse(process.argv);
|
|
@@ -273,9 +273,38 @@ export class CanvasClient {
|
|
|
273
273
|
return response.data;
|
|
274
274
|
}
|
|
275
275
|
async updateModule(courseId, moduleId, data) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
276
|
+
// Canvas only accepts arrays (prerequisite_module_ids, completion_requirements)
|
|
277
|
+
// via form-encoding, not JSON.
|
|
278
|
+
const hasArrays = data.prerequisite_module_ids !== undefined || data.completion_requirements !== undefined;
|
|
279
|
+
if (hasArrays) {
|
|
280
|
+
const params = new URLSearchParams();
|
|
281
|
+
if (data.name !== undefined)
|
|
282
|
+
params.set('module[name]', data.name);
|
|
283
|
+
if (data.published !== undefined)
|
|
284
|
+
params.set('module[published]', String(data.published));
|
|
285
|
+
if (data.position !== undefined)
|
|
286
|
+
params.set('module[position]', String(data.position));
|
|
287
|
+
if (data.require_sequential_progress !== undefined) {
|
|
288
|
+
params.set('module[require_sequential_progress]', String(data.require_sequential_progress));
|
|
289
|
+
}
|
|
290
|
+
if (data.prerequisite_module_ids) {
|
|
291
|
+
for (const id of data.prerequisite_module_ids) {
|
|
292
|
+
params.append('module[prerequisite_module_ids][]', String(id));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (data.completion_requirements) {
|
|
296
|
+
for (const req of data.completion_requirements) {
|
|
297
|
+
params.append('module[completion_requirements][][id]', String(req.id));
|
|
298
|
+
params.append('module[completion_requirements][][type]', req.type);
|
|
299
|
+
if (req.min_score !== undefined) {
|
|
300
|
+
params.append('module[completion_requirements][][min_score]', String(req.min_score));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const response = await this.client.put(`courses/${courseId}/modules/${moduleId}`, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
|
305
|
+
return response.data;
|
|
306
|
+
}
|
|
307
|
+
const response = await this.client.put(`courses/${courseId}/modules/${moduleId}`, { module: data });
|
|
279
308
|
return response.data;
|
|
280
309
|
}
|
|
281
310
|
async deleteModule(courseId, moduleId) {
|
|
@@ -290,9 +319,20 @@ export class CanvasClient {
|
|
|
290
319
|
return response.data;
|
|
291
320
|
}
|
|
292
321
|
async updateModuleItem(courseId, moduleId, itemId, data) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
322
|
+
// Canvas only accepts completion_requirement via form-encoding, not JSON.
|
|
323
|
+
// Use URLSearchParams when completion_requirement is present.
|
|
324
|
+
if (data.completion_requirement) {
|
|
325
|
+
const params = new URLSearchParams();
|
|
326
|
+
params.set('module_item[completion_requirement][type]', data.completion_requirement.type);
|
|
327
|
+
const { completion_requirement, ...rest } = data;
|
|
328
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
329
|
+
if (value !== undefined)
|
|
330
|
+
params.set(`module_item[${key}]`, String(value));
|
|
331
|
+
}
|
|
332
|
+
const response = await this.client.put(`courses/${courseId}/modules/${moduleId}/items/${itemId}`, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
|
333
|
+
return response.data;
|
|
334
|
+
}
|
|
335
|
+
const response = await this.client.put(`courses/${courseId}/modules/${moduleId}/items/${itemId}`, { module_item: data });
|
|
296
336
|
return response.data;
|
|
297
337
|
}
|
|
298
338
|
async deleteModuleItem(courseId, moduleId, itemId) {
|
|
@@ -333,6 +373,39 @@ export class CanvasClient {
|
|
|
333
373
|
}
|
|
334
374
|
return uploadResponse.data;
|
|
335
375
|
}
|
|
376
|
+
async getQuizSubmissions(courseId, quizId) {
|
|
377
|
+
const response = await this.client.get(`courses/${courseId}/quizzes/${quizId}/submissions`, {
|
|
378
|
+
params: { include: ['user'], per_page: 100 }
|
|
379
|
+
});
|
|
380
|
+
return response.data.quiz_submissions ?? [];
|
|
381
|
+
}
|
|
382
|
+
async deleteQuiz(courseId, quizId) {
|
|
383
|
+
await this.client.delete(`courses/${courseId}/quizzes/${quizId}`);
|
|
384
|
+
return { deleted: true };
|
|
385
|
+
}
|
|
386
|
+
async checkQuizPending(courseId, quizId) {
|
|
387
|
+
const [students, submissionsResp] = await Promise.all([
|
|
388
|
+
this.getEnrollments(courseId),
|
|
389
|
+
this.client.get(`courses/${courseId}/quizzes/${quizId}/submissions`, {
|
|
390
|
+
params: { per_page: 100 }
|
|
391
|
+
})
|
|
392
|
+
]);
|
|
393
|
+
const realStudents = students.filter(u => u.name !== 'Estudiante de prueba' && u.sis_user_id !== null);
|
|
394
|
+
const submittedIds = new Set((submissionsResp.data.quiz_submissions ?? []).map((s) => s.user_id));
|
|
395
|
+
const pending = realStudents
|
|
396
|
+
.filter(u => !submittedIds.has(u.id))
|
|
397
|
+
.map(u => ({
|
|
398
|
+
id: u.id,
|
|
399
|
+
name: u.name ?? 'Desconocido',
|
|
400
|
+
email: u.login_id ?? ''
|
|
401
|
+
}));
|
|
402
|
+
return {
|
|
403
|
+
quiz_id: quizId,
|
|
404
|
+
total_students: realStudents.length,
|
|
405
|
+
submitted: submittedIds.size,
|
|
406
|
+
pending
|
|
407
|
+
};
|
|
408
|
+
}
|
|
336
409
|
async createQuiz(courseId, quiz) {
|
|
337
410
|
const response = await this.client.post(`courses/${courseId}/quizzes`, {
|
|
338
411
|
quiz: quiz
|
|
@@ -63,7 +63,7 @@ export const moduleTools = [
|
|
|
63
63
|
name: "canvas_update_module",
|
|
64
64
|
tool: {
|
|
65
65
|
name: "canvas_update_module",
|
|
66
|
-
description: "Update an existing module",
|
|
66
|
+
description: "Update an existing module (name, published, position, prerequisites, completion requirements)",
|
|
67
67
|
inputSchema: {
|
|
68
68
|
type: "object",
|
|
69
69
|
properties: {
|
|
@@ -74,7 +74,32 @@ export const moduleTools = [
|
|
|
74
74
|
module_id: { type: "number", description: "The ID of the module" },
|
|
75
75
|
name: { type: "string", description: "The new name of the module" },
|
|
76
76
|
published: { type: "boolean", description: "Whether the module is published" },
|
|
77
|
-
position: { type: "number", description: "The new position of the module" }
|
|
77
|
+
position: { type: "number", description: "The new position of the module" },
|
|
78
|
+
prerequisite_module_ids: {
|
|
79
|
+
type: "array",
|
|
80
|
+
items: { type: "number" },
|
|
81
|
+
description: "IDs of modules that must be completed before this one unlocks"
|
|
82
|
+
},
|
|
83
|
+
require_sequential_progress: {
|
|
84
|
+
type: "boolean",
|
|
85
|
+
description: "Whether items in the module must be completed in order"
|
|
86
|
+
},
|
|
87
|
+
completion_requirements: {
|
|
88
|
+
type: "array",
|
|
89
|
+
description: "Completion requirements for module items. Replaces ALL existing requirements — include every item that needs one.",
|
|
90
|
+
items: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
id: { type: "number", description: "The module item ID" },
|
|
94
|
+
type: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "must_view, must_submit, must_contribute, min_score, must_mark_done"
|
|
97
|
+
},
|
|
98
|
+
min_score: { type: "number", description: "Required when type is min_score" }
|
|
99
|
+
},
|
|
100
|
+
required: ["id", "type"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
78
103
|
},
|
|
79
104
|
required: ["course_id", "module_id"],
|
|
80
105
|
},
|
|
@@ -85,13 +110,23 @@ export const moduleTools = [
|
|
|
85
110
|
module_id: z.number(),
|
|
86
111
|
name: z.string().optional(),
|
|
87
112
|
published: z.boolean().optional(),
|
|
88
|
-
position: z.number().optional()
|
|
113
|
+
position: z.number().optional(),
|
|
114
|
+
prerequisite_module_ids: z.array(z.number()).optional(),
|
|
115
|
+
require_sequential_progress: z.boolean().optional(),
|
|
116
|
+
completion_requirements: z.array(z.object({
|
|
117
|
+
id: z.number(),
|
|
118
|
+
type: z.string(),
|
|
119
|
+
min_score: z.number().optional()
|
|
120
|
+
})).optional()
|
|
89
121
|
}).parse(args);
|
|
90
122
|
const courseId = await resolveCourseId(client, input.course_id);
|
|
91
123
|
const module = await client.updateModule(courseId, input.module_id, {
|
|
92
124
|
name: input.name,
|
|
93
125
|
published: input.published,
|
|
94
|
-
position: input.position
|
|
126
|
+
position: input.position,
|
|
127
|
+
prerequisite_module_ids: input.prerequisite_module_ids,
|
|
128
|
+
require_sequential_progress: input.require_sequential_progress,
|
|
129
|
+
completion_requirements: input.completion_requirements
|
|
95
130
|
});
|
|
96
131
|
return {
|
|
97
132
|
content: [{ type: "text", text: JSON.stringify(module, null, 2) }],
|
|
@@ -206,7 +241,11 @@ export const moduleTools = [
|
|
|
206
241
|
position: { type: "number", description: "The new position of the item" },
|
|
207
242
|
indent: { type: "number", description: "The new level of indentation (0-3)" },
|
|
208
243
|
published: { type: "boolean", description: "Whether the item is published" },
|
|
209
|
-
new_module_id: { type: "number", description: "Move the item to a new module" }
|
|
244
|
+
new_module_id: { type: "number", description: "Move the item to a new module" },
|
|
245
|
+
completion_requirement_type: {
|
|
246
|
+
type: "string",
|
|
247
|
+
description: "Completion requirement type: must_view, must_submit, must_contribute, min_score, must_mark_done"
|
|
248
|
+
}
|
|
210
249
|
},
|
|
211
250
|
required: ["course_id", "module_id", "item_id"],
|
|
212
251
|
},
|
|
@@ -220,7 +259,8 @@ export const moduleTools = [
|
|
|
220
259
|
position: z.number().optional(),
|
|
221
260
|
indent: z.number().optional(),
|
|
222
261
|
published: z.boolean().optional(),
|
|
223
|
-
new_module_id: z.number().optional()
|
|
262
|
+
new_module_id: z.number().optional(),
|
|
263
|
+
completion_requirement_type: z.string().optional()
|
|
224
264
|
}).parse(args);
|
|
225
265
|
const courseId = await resolveCourseId(client, input.course_id);
|
|
226
266
|
const result = await client.updateModuleItem(courseId, input.module_id, input.item_id, {
|
|
@@ -228,7 +268,10 @@ export const moduleTools = [
|
|
|
228
268
|
position: input.position,
|
|
229
269
|
indent: input.indent,
|
|
230
270
|
published: input.published,
|
|
231
|
-
module_id: input.new_module_id
|
|
271
|
+
module_id: input.new_module_id,
|
|
272
|
+
...(input.completion_requirement_type && {
|
|
273
|
+
completion_requirement: { type: input.completion_requirement_type }
|
|
274
|
+
})
|
|
232
275
|
});
|
|
233
276
|
return {
|
|
234
277
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { resolveCourseId } from "../common/helpers.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const questionBankTools = [
|
|
4
|
+
{
|
|
5
|
+
name: "canvas_list_question_banks",
|
|
6
|
+
tool: {
|
|
7
|
+
name: "canvas_list_question_banks",
|
|
8
|
+
description: "List question banks 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({ course_id: z.union([z.number(), z.string()]) }).parse(args);
|
|
22
|
+
const courseId = await resolveCourseId(client, input.course_id);
|
|
23
|
+
const banks = await client.listQuestionBanks(courseId);
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: JSON.stringify(banks, null, 2) }]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "canvas_create_question_bank",
|
|
31
|
+
tool: {
|
|
32
|
+
name: "canvas_create_question_bank",
|
|
33
|
+
description: "Create a new question bank in a course",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
course_id: {
|
|
38
|
+
anyOf: [{ type: "number" }, { type: "string" }],
|
|
39
|
+
description: "The ID or name of the course"
|
|
40
|
+
},
|
|
41
|
+
title: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Title for the question bank"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
required: ["course_id", "title"]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
handler: async (client, args) => {
|
|
50
|
+
const input = z.object({
|
|
51
|
+
course_id: z.union([z.number(), z.string()]),
|
|
52
|
+
title: z.string().min(1)
|
|
53
|
+
}).parse(args);
|
|
54
|
+
const courseId = await resolveCourseId(client, input.course_id);
|
|
55
|
+
const bank = await client.createQuestionBank(courseId, input.title);
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify(bank, null, 2) }]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "canvas_create_assessment_question",
|
|
63
|
+
tool: {
|
|
64
|
+
name: "canvas_create_assessment_question",
|
|
65
|
+
description: "Create a question in a question bank. 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.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
course_id: {
|
|
70
|
+
anyOf: [{ type: "number" }, { type: "string" }],
|
|
71
|
+
description: "The ID or name of the course"
|
|
72
|
+
},
|
|
73
|
+
bank_id: {
|
|
74
|
+
type: "number",
|
|
75
|
+
description: "The question bank ID"
|
|
76
|
+
},
|
|
77
|
+
name: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Short name/title for the question"
|
|
80
|
+
},
|
|
81
|
+
question_type: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "Question type (e.g. multiple_choice_question, true_false_question, essay_question, short_answer_question)"
|
|
84
|
+
},
|
|
85
|
+
question_text: {
|
|
86
|
+
type: "string",
|
|
87
|
+
description: "The question text (HTML supported)"
|
|
88
|
+
},
|
|
89
|
+
points_possible: {
|
|
90
|
+
type: "number",
|
|
91
|
+
description: "Point value for the question"
|
|
92
|
+
},
|
|
93
|
+
answers: {
|
|
94
|
+
type: "array",
|
|
95
|
+
description: "Array of answer objects. For multiple_choice: weight=100 for correct, weight=0 for incorrect. For true_false: text='True'/'False'.",
|
|
96
|
+
items: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
text: { type: "string", description: "Answer text" },
|
|
100
|
+
weight: { type: "number", description: "100 for correct, 0 for incorrect" },
|
|
101
|
+
comments: { type: "string", description: "Optional feedback comment" }
|
|
102
|
+
},
|
|
103
|
+
required: ["text", "weight"]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
required: ["course_id", "bank_id", "name", "question_type", "question_text", "points_possible"]
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
handler: async (client, args) => {
|
|
111
|
+
const answerSchema = z.object({
|
|
112
|
+
text: z.string(),
|
|
113
|
+
weight: z.number(),
|
|
114
|
+
comments: z.string().optional()
|
|
115
|
+
});
|
|
116
|
+
const input = z.object({
|
|
117
|
+
course_id: z.union([z.number(), z.string()]),
|
|
118
|
+
bank_id: z.coerce.number(),
|
|
119
|
+
name: z.string().min(1),
|
|
120
|
+
question_type: z.string().min(1),
|
|
121
|
+
question_text: z.string().min(1),
|
|
122
|
+
points_possible: z.coerce.number(),
|
|
123
|
+
answers: z.array(answerSchema).optional()
|
|
124
|
+
}).parse(args);
|
|
125
|
+
const courseId = await resolveCourseId(client, input.course_id);
|
|
126
|
+
const question = await client.createAssessmentQuestion(courseId, input.bank_id, {
|
|
127
|
+
name: input.name,
|
|
128
|
+
question_type: input.question_type,
|
|
129
|
+
question_text: input.question_text,
|
|
130
|
+
points_possible: input.points_possible,
|
|
131
|
+
answers: input.answers
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: JSON.stringify(question, null, 2) }]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "canvas_move_question_bank_questions",
|
|
140
|
+
tool: {
|
|
141
|
+
name: "canvas_move_question_bank_questions",
|
|
142
|
+
description: "Move questions from one question bank to another. If question_ids is omitted, all questions are moved.",
|
|
143
|
+
inputSchema: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
course_id: {
|
|
147
|
+
anyOf: [{ type: "number" }, { type: "string" }],
|
|
148
|
+
description: "The ID or name of the course"
|
|
149
|
+
},
|
|
150
|
+
source_bank_id: {
|
|
151
|
+
type: "number",
|
|
152
|
+
description: "Source question bank ID"
|
|
153
|
+
},
|
|
154
|
+
dest_bank_id: {
|
|
155
|
+
type: "number",
|
|
156
|
+
description: "Destination question bank ID"
|
|
157
|
+
},
|
|
158
|
+
question_ids: {
|
|
159
|
+
type: "array",
|
|
160
|
+
items: { type: "number" },
|
|
161
|
+
description: "Optional list of specific question IDs to move. If omitted, all questions are moved."
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
required: ["course_id", "source_bank_id", "dest_bank_id"]
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
handler: async (client, args) => {
|
|
168
|
+
const input = z.object({
|
|
169
|
+
course_id: z.union([z.number(), z.string()]),
|
|
170
|
+
source_bank_id: z.coerce.number(),
|
|
171
|
+
dest_bank_id: z.coerce.number(),
|
|
172
|
+
question_ids: z.array(z.coerce.number()).optional()
|
|
173
|
+
}).parse(args);
|
|
174
|
+
const courseId = await resolveCourseId(client, input.course_id);
|
|
175
|
+
const result = await client.moveQuestionBankQuestions(courseId, input.source_bank_id, input.dest_bank_id, input.question_ids);
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "canvas_create_quiz_group",
|
|
183
|
+
tool: {
|
|
184
|
+
name: "canvas_create_quiz_group",
|
|
185
|
+
description: "Create a quiz group linked to a question bank. The group randomly picks questions from the bank when students take the quiz.",
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {
|
|
189
|
+
course_id: {
|
|
190
|
+
anyOf: [{ type: "number" }, { type: "string" }],
|
|
191
|
+
description: "The ID or name of the course"
|
|
192
|
+
},
|
|
193
|
+
quiz_id: {
|
|
194
|
+
type: "number",
|
|
195
|
+
description: "The quiz ID"
|
|
196
|
+
},
|
|
197
|
+
name: {
|
|
198
|
+
type: "string",
|
|
199
|
+
description: "Name for the quiz group"
|
|
200
|
+
},
|
|
201
|
+
pick_count: {
|
|
202
|
+
type: "number",
|
|
203
|
+
description: "Number of questions to randomly pick from the bank"
|
|
204
|
+
},
|
|
205
|
+
question_points: {
|
|
206
|
+
type: "number",
|
|
207
|
+
description: "Points per question in this group"
|
|
208
|
+
},
|
|
209
|
+
assessment_question_bank_id: {
|
|
210
|
+
type: "number",
|
|
211
|
+
description: "The question bank ID to link to this group"
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
required: ["course_id", "quiz_id", "name", "pick_count", "question_points"]
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
handler: async (client, args) => {
|
|
218
|
+
const input = z.object({
|
|
219
|
+
course_id: z.union([z.number(), z.string()]),
|
|
220
|
+
quiz_id: z.coerce.number(),
|
|
221
|
+
name: z.string().min(1),
|
|
222
|
+
pick_count: z.coerce.number().min(1),
|
|
223
|
+
question_points: z.coerce.number().min(0),
|
|
224
|
+
assessment_question_bank_id: z.coerce.number().optional()
|
|
225
|
+
}).parse(args);
|
|
226
|
+
const courseId = await resolveCourseId(client, input.course_id);
|
|
227
|
+
const group = await client.createQuizGroup(courseId, input.quiz_id, {
|
|
228
|
+
name: input.name,
|
|
229
|
+
pick_count: input.pick_count,
|
|
230
|
+
question_points: input.question_points,
|
|
231
|
+
assessment_question_bank_id: input.assessment_question_bank_id
|
|
232
|
+
});
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: JSON.stringify(group, null, 2) }]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
];
|
package/dist/tools/quiz-tools.js
CHANGED
|
@@ -437,6 +437,78 @@ export const quizTools = [
|
|
|
437
437
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
438
438
|
}
|
|
439
439
|
},
|
|
440
|
+
{
|
|
441
|
+
name: "canvas_get_quiz_submissions",
|
|
442
|
+
tool: {
|
|
443
|
+
name: "canvas_get_quiz_submissions",
|
|
444
|
+
description: "Get all submissions for a quiz, including user info. Useful to see who has responded.",
|
|
445
|
+
inputSchema: {
|
|
446
|
+
type: "object",
|
|
447
|
+
properties: {
|
|
448
|
+
course_id: { anyOf: [{ type: "number" }, { type: "string" }], description: "The ID or name of the course" },
|
|
449
|
+
quiz_id: { type: "number", description: "The quiz ID" }
|
|
450
|
+
},
|
|
451
|
+
required: ["course_id", "quiz_id"]
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
handler: async (client, args) => {
|
|
455
|
+
const input = z.object({
|
|
456
|
+
course_id: z.union([z.number(), z.string()]),
|
|
457
|
+
quiz_id: z.number()
|
|
458
|
+
}).parse(args);
|
|
459
|
+
const courseId = await resolveCourseId(client, input.course_id);
|
|
460
|
+
const submissions = await client.getQuizSubmissions(courseId, input.quiz_id);
|
|
461
|
+
return { content: [{ type: "text", text: JSON.stringify(submissions, null, 2) }] };
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "canvas_check_quiz_pending",
|
|
466
|
+
tool: {
|
|
467
|
+
name: "canvas_check_quiz_pending",
|
|
468
|
+
description: "Check which enrolled students have NOT submitted a quiz. Returns a list of pending students with their name and email. Excludes test accounts.",
|
|
469
|
+
inputSchema: {
|
|
470
|
+
type: "object",
|
|
471
|
+
properties: {
|
|
472
|
+
course_id: { anyOf: [{ type: "number" }, { type: "string" }], description: "The ID or name of the course" },
|
|
473
|
+
quiz_id: { type: "number", description: "The quiz ID" }
|
|
474
|
+
},
|
|
475
|
+
required: ["course_id", "quiz_id"]
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
handler: async (client, args) => {
|
|
479
|
+
const input = z.object({
|
|
480
|
+
course_id: z.union([z.number(), z.string()]),
|
|
481
|
+
quiz_id: z.number()
|
|
482
|
+
}).parse(args);
|
|
483
|
+
const courseId = await resolveCourseId(client, input.course_id);
|
|
484
|
+
const result = await client.checkQuizPending(courseId, input.quiz_id);
|
|
485
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: "canvas_delete_quiz",
|
|
490
|
+
tool: {
|
|
491
|
+
name: "canvas_delete_quiz",
|
|
492
|
+
description: "Permanently delete a classic quiz from a course. This cannot be undone.",
|
|
493
|
+
inputSchema: {
|
|
494
|
+
type: "object",
|
|
495
|
+
properties: {
|
|
496
|
+
course_id: { anyOf: [{ type: "number" }, { type: "string" }], description: "The ID or name of the course" },
|
|
497
|
+
quiz_id: { type: "number", description: "The quiz ID to delete" }
|
|
498
|
+
},
|
|
499
|
+
required: ["course_id", "quiz_id"]
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
handler: async (client, args) => {
|
|
503
|
+
const input = z.object({
|
|
504
|
+
course_id: z.union([z.number(), z.string()]),
|
|
505
|
+
quiz_id: z.number()
|
|
506
|
+
}).parse(args);
|
|
507
|
+
const courseId = await resolveCourseId(client, input.course_id);
|
|
508
|
+
const result = await client.deleteQuiz(courseId, input.quiz_id);
|
|
509
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
510
|
+
}
|
|
511
|
+
},
|
|
440
512
|
{
|
|
441
513
|
name: "canvas_update_quiz",
|
|
442
514
|
tool: {
|
package/package.json
CHANGED
|
@@ -1,70 +1,76 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@charlie.act7/canvas-mcp-server",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "git+https://github.com/CharlieCardenasToledo/mcp-canvas-server.git"
|
|
6
|
+
},
|
|
7
|
+
"homepage": "https://github.com/CharlieCardenasToledo/mcp-canvas-server#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/CharlieCardenasToledo/mcp-canvas-server/issues"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"version": "1.2.1",
|
|
15
|
+
"description": "MCP Server for Canvas LMS - Use AI to interact with your courses, assignments, and grades.",
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"bin": {
|
|
18
|
+
"canvas-mcp": "dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/index.js",
|
|
23
|
+
"dist/http-server.js",
|
|
24
|
+
"dist/common",
|
|
25
|
+
"dist/prompts",
|
|
26
|
+
"dist/resources",
|
|
27
|
+
"dist/services",
|
|
28
|
+
"dist/tools",
|
|
29
|
+
"README.md",
|
|
30
|
+
"llms-install.md"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"start": "node dist/index.js",
|
|
35
|
+
"start:http": "node dist/index.js serve-http",
|
|
36
|
+
"dev": "tsc --watch",
|
|
37
|
+
"chat": "tsx src/ollama_bridge.ts",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"mcp",
|
|
42
|
+
"canvas",
|
|
43
|
+
"lms",
|
|
44
|
+
"ai",
|
|
45
|
+
"agent",
|
|
46
|
+
"server"
|
|
47
|
+
],
|
|
48
|
+
"author": "Charlie Cárdenas Toledo",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@fastify/swagger": "^9.5.2",
|
|
52
|
+
"@fastify/swagger-ui": "^5.2.3",
|
|
53
|
+
"@google/genai": "^2.8.0",
|
|
54
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
55
|
+
"axios": "^1.6.0",
|
|
56
|
+
"chalk": "^5.3.0",
|
|
57
|
+
"commander": "^11.1.0",
|
|
58
|
+
"conf": "^12.0.0",
|
|
59
|
+
"dotenv": "^16.3.1",
|
|
60
|
+
"fastify": "^5.6.1",
|
|
61
|
+
"form-data": "^4.0.5",
|
|
62
|
+
"inquirer": "^9.2.12",
|
|
63
|
+
"ollama": "^0.6.3",
|
|
64
|
+
"zod": "^3.22.4"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@types/inquirer": "^9.0.7",
|
|
68
|
+
"@types/node": "^20.10.0",
|
|
69
|
+
"nodemon": "^3.1.11",
|
|
70
|
+
"tsx": "^4.21.0",
|
|
71
|
+
"typescript": "^5.3.0"
|
|
72
|
+
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"node": ">=18.0.0"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "canvas-lms",
|
|
3
|
-
"description": "Canvas LMS integration for teachers. Manage courses, assignments, grades, quizzes, students, modules, and more — directly from Claude Code.",
|
|
4
|
-
"version": "1.1.0",
|
|
5
|
-
"author": {
|
|
6
|
-
"name": "CharlieCardenasToledo"
|
|
7
|
-
},
|
|
8
|
-
"homepage": "https://github.com/CharlieCardenasToledo/canvas-mcp-server",
|
|
9
|
-
"repository": "https://github.com/CharlieCardenasToledo/canvas-mcp-server",
|
|
10
|
-
"license": "MIT"
|
|
11
|
-
}
|