@charlie.act7/canvas-mcp-server 1.2.0 → 1.2.2

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,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
+ ];
@@ -196,18 +196,18 @@ export const quizTools = [
196
196
  name: "canvas_create_quiz_question",
197
197
  tool: {
198
198
  name: "canvas_create_quiz_question",
199
- description: `Create a new question in a quiz. Supports all 12 Canvas question types:
200
- - multiple_choice_question: answers with answer_text + answer_weight (100=correct, 0=wrong)
201
- - true_false_question: two answers ("True"/"False") with answer_weight
202
- - short_answer_question: one or more correct answer_text values, all with answer_weight 100
203
- - fill_in_multiple_blanks_question: answers with blank_id + answer_text + answer_weight 100
204
- - multiple_answers_question: checkboxes; correct answers get answer_weight 100, wrong get 0
205
- - multiple_dropdowns_question: answers with blank_id + answer_text + answer_weight
206
- - matching_question: answers with answer_match_left + answer_match_right; add distractors via matching_answer_incorrect_matches on any one answer
207
- - numerical_question: answers with numerical_answer_type (exact_answer: exact+margin; range_answer: start+end; precision_answer: approximate+precision)
208
- - calculated_question: answers with variables and formulas (advanced)
209
- - essay_question: no answers needed
210
- - file_upload_question: no answers needed
199
+ description: `Create a new question in a quiz. Supports all 12 Canvas question types:
200
+ - multiple_choice_question: answers with answer_text + answer_weight (100=correct, 0=wrong)
201
+ - true_false_question: two answers ("True"/"False") with answer_weight
202
+ - short_answer_question: one or more correct answer_text values, all with answer_weight 100
203
+ - fill_in_multiple_blanks_question: answers with blank_id + answer_text + answer_weight 100
204
+ - multiple_answers_question: checkboxes; correct answers get answer_weight 100, wrong get 0
205
+ - multiple_dropdowns_question: answers with blank_id + answer_text + answer_weight
206
+ - matching_question: answers with answer_match_left + answer_match_right; add distractors via matching_answer_incorrect_matches on any one answer
207
+ - numerical_question: answers with numerical_answer_type (exact_answer: exact+margin; range_answer: start+end; precision_answer: approximate+precision)
208
+ - calculated_question: answers with variables and formulas (advanced)
209
+ - essay_question: no answers needed
210
+ - file_upload_question: no answers needed
211
211
  - text_only_question: no answers needed, just displays text`,
212
212
  inputSchema: {
213
213
  type: "object",
@@ -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
- "name": "@charlie.act7/canvas-mcp-server",
3
- "publishConfig": {
4
- "access": "public"
5
- },
6
- "version": "1.2.0",
7
- "description": "MCP Server for Canvas LMS - Use AI to interact with your courses, assignments, and grades.",
8
- "main": "dist/index.js",
9
- "bin": {
10
- "canvas-mcp": "dist/index.js"
11
- },
12
- "type": "module",
13
- "files": [
14
- "dist/index.js",
15
- "dist/http-server.js",
16
- "dist/common",
17
- "dist/prompts",
18
- "dist/resources",
19
- "dist/services",
20
- "dist/tools",
21
- "README.md",
22
- "llms-install.md",
23
- ".claude-plugin",
24
- ".mcp.json"
25
- ],
26
- "scripts": {
27
- "build": "tsc",
28
- "start": "node dist/index.js",
29
- "start:http": "node dist/index.js serve-http",
30
- "dev": "tsc --watch",
31
- "chat": "tsx src/ollama_bridge.ts",
32
- "prepublishOnly": "npm run build"
33
- },
34
- "keywords": [
35
- "mcp",
36
- "canvas",
37
- "lms",
38
- "ai",
39
- "agent",
40
- "server"
41
- ],
42
- "author": "Charlie Cárdenas Toledo",
43
- "license": "MIT",
44
- "dependencies": {
45
- "@fastify/swagger": "^9.5.2",
46
- "@fastify/swagger-ui": "^5.2.3",
47
- "@google/genai": "^2.8.0",
48
- "@modelcontextprotocol/sdk": "^1.25.3",
49
- "axios": "^1.6.0",
50
- "chalk": "^5.3.0",
51
- "commander": "^11.1.0",
52
- "conf": "^12.0.0",
53
- "dotenv": "^16.3.1",
54
- "fastify": "^5.6.1",
55
- "form-data": "^4.0.5",
56
- "inquirer": "^9.2.12",
57
- "ollama": "^0.6.3",
58
- "zod": "^3.22.4"
59
- },
60
- "devDependencies": {
61
- "@types/inquirer": "^9.0.7",
62
- "@types/node": "^20.10.0",
63
- "nodemon": "^3.1.11",
64
- "tsx": "^4.21.0",
65
- "typescript": "^5.3.0"
66
- },
67
- "engines": {
68
- "node": ">=18.0.0"
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.2",
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
- }
package/.mcp.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "canvas": {
4
- "command": "npx",
5
- "args": ["-y", "@charlie.act7/canvas-mcp-server"],
6
- "env": {
7
- "CANVAS_API_TOKEN": "${CANVAS_API_TOKEN}",
8
- "CANVAS_API_DOMAIN": "${CANVAS_API_DOMAIN}"
9
- }
10
- }
11
- }
12
- }