@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,229 @@
1
+ import { resolveCourseId } from "../common/helpers.js";
2
+ import { z } from "zod";
3
+ export const fileTools = [
4
+ {
5
+ name: "canvas_upload_file",
6
+ tool: {
7
+ name: "canvas_upload_file",
8
+ description: "Upload a local file to a Canvas course and optionally add it to a module",
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
+ file_path: { type: "string", description: "The local path to the file to upload" },
17
+ file_name: { type: "string", description: "The name to give the file in Canvas" },
18
+ content_type: { type: "string", description: "The MIME type of the file (e.g., application/pdf)" },
19
+ parent_folder_id: { type: "number", description: "Optional folder ID in Canvas to upload to" },
20
+ module_id: { type: "number", description: "Optional: Add the uploaded file to this module" },
21
+ position: { type: "number", description: "Optional position in the module" },
22
+ indent: { type: "number", description: "Optional level of indentation (0-3)" }
23
+ },
24
+ required: ["course_id", "file_path", "file_name", "content_type"],
25
+ },
26
+ },
27
+ handler: async (client, args) => {
28
+ const input = z.object({
29
+ course_id: z.union([z.number(), z.string()]),
30
+ file_path: z.string(),
31
+ file_name: z.string(),
32
+ content_type: z.string(),
33
+ parent_folder_id: z.number().optional(),
34
+ module_id: z.number().optional(),
35
+ position: z.number().optional(),
36
+ indent: z.number().optional()
37
+ }).parse(args);
38
+ const courseId = await resolveCourseId(client, input.course_id);
39
+ // 1. Upload the file
40
+ const file = await client.uploadFile(courseId, input.file_path, input.file_name, input.content_type, input.parent_folder_id);
41
+ let resultText = `File uploaded successfully: ${file.display_name} (ID: ${file.id})`;
42
+ // 2. Optionally add to module
43
+ if (input.module_id && file.id) {
44
+ const moduleItem = await client.createModuleItem(courseId, input.module_id, {
45
+ type: 'File',
46
+ content_id: file.id,
47
+ position: input.position,
48
+ indent: input.indent
49
+ });
50
+ resultText += `\nAnd added to module ${input.module_id} as item ${moduleItem.id}`;
51
+ }
52
+ return {
53
+ content: [{ type: "text", text: resultText }],
54
+ };
55
+ }
56
+ },
57
+ {
58
+ name: "canvas_list_folders",
59
+ tool: {
60
+ name: "canvas_list_folders",
61
+ description: "List folders in a Canvas course",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ course_id: {
66
+ anyOf: [{ type: "number" }, { type: "string" }],
67
+ description: "The ID or name of the course"
68
+ }
69
+ },
70
+ required: ["course_id"],
71
+ },
72
+ },
73
+ handler: async (client, args) => {
74
+ const input = z.object({
75
+ course_id: z.union([z.number(), z.string()])
76
+ }).parse(args);
77
+ const courseId = await resolveCourseId(client, input.course_id);
78
+ const folders = await client.getFolders(courseId);
79
+ return {
80
+ content: [{ type: "text", text: JSON.stringify(folders, null, 2) }],
81
+ };
82
+ }
83
+ },
84
+ {
85
+ name: "canvas_create_folder",
86
+ tool: {
87
+ name: "canvas_create_folder",
88
+ description: "Create a new folder in a Canvas course",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ course_id: {
93
+ anyOf: [{ type: "number" }, { type: "string" }],
94
+ description: "The ID or name of the course"
95
+ },
96
+ name: { type: "string", description: "The name of the new folder" },
97
+ parent_folder_id: { type: "number", description: "Optional parent folder ID" }
98
+ },
99
+ required: ["course_id", "name"],
100
+ },
101
+ },
102
+ handler: async (client, args) => {
103
+ const input = z.object({
104
+ course_id: z.union([z.number(), z.string()]),
105
+ name: z.string(),
106
+ parent_folder_id: z.number().optional()
107
+ }).parse(args);
108
+ const courseId = await resolveCourseId(client, input.course_id);
109
+ const folder = await client.createFolder(courseId, input.name, input.parent_folder_id);
110
+ return {
111
+ content: [{ type: "text", text: `Folder created successfully: ${folder.name} (ID: ${folder.id})` }],
112
+ };
113
+ }
114
+ },
115
+ {
116
+ name: "canvas_update_folder",
117
+ tool: {
118
+ name: "canvas_update_folder",
119
+ description: "Update a folder's name or location",
120
+ inputSchema: {
121
+ type: "object",
122
+ properties: {
123
+ folder_id: { type: "number", description: "The ID of the folder to update" },
124
+ name: { type: "string", description: "The new name for the folder" },
125
+ parent_folder_id: { type: "number", description: "The ID of the new parent folder" }
126
+ },
127
+ required: ["folder_id"],
128
+ },
129
+ },
130
+ handler: async (client, args) => {
131
+ const input = z.object({
132
+ folder_id: z.number(),
133
+ name: z.string().optional(),
134
+ parent_folder_id: z.number().optional()
135
+ }).parse(args);
136
+ const folder = await client.updateFolder(input.folder_id, {
137
+ name: input.name,
138
+ parent_folder_id: input.parent_folder_id
139
+ });
140
+ return {
141
+ content: [{ type: "text", text: `Folder updated successfully: ${folder.name} (ID: ${folder.id})` }],
142
+ };
143
+ }
144
+ },
145
+ {
146
+ name: "canvas_delete_folder",
147
+ tool: {
148
+ name: "canvas_delete_folder",
149
+ description: "Delete a folder from Canvas",
150
+ inputSchema: {
151
+ type: "object",
152
+ properties: {
153
+ folder_id: { type: "number", description: "The ID of the folder to delete" },
154
+ force: { type: "boolean", description: "Set to true to delete even if folder is not empty" }
155
+ },
156
+ required: ["folder_id"],
157
+ },
158
+ },
159
+ handler: async (client, args) => {
160
+ const input = z.object({
161
+ folder_id: z.number(),
162
+ force: z.boolean().optional().default(false)
163
+ }).parse(args);
164
+ await client.deleteFolder(input.folder_id, input.force);
165
+ return {
166
+ content: [{ type: "text", text: `Folder ${input.folder_id} deleted successfully.` }],
167
+ };
168
+ }
169
+ },
170
+ {
171
+ name: "canvas_update_file",
172
+ tool: {
173
+ name: "canvas_update_file",
174
+ description: "Update a file's metadata (name, folder, visibility)",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ file_id: { type: "number", description: "The ID of the file to update" },
179
+ name: { type: "string", description: "The new name for the file" },
180
+ parent_folder_id: { type: "number", description: "The ID of the new parent folder" },
181
+ locked: { type: "boolean", description: "Whether the file is locked" },
182
+ hidden: { type: "boolean", description: "Whether the file is hidden" }
183
+ },
184
+ required: ["file_id"],
185
+ },
186
+ },
187
+ handler: async (client, args) => {
188
+ const input = z.object({
189
+ file_id: z.number(),
190
+ name: z.string().optional(),
191
+ parent_folder_id: z.number().optional(),
192
+ locked: z.boolean().optional(),
193
+ hidden: z.boolean().optional()
194
+ }).parse(args);
195
+ const file = await client.updateFile(input.file_id, {
196
+ name: input.name,
197
+ parent_folder_id: input.parent_folder_id,
198
+ locked: input.locked,
199
+ hidden: input.hidden
200
+ });
201
+ return {
202
+ content: [{ type: "text", text: `File updated successfully: ${file.display_name} (ID: ${file.id})` }],
203
+ };
204
+ }
205
+ },
206
+ {
207
+ name: "canvas_delete_file",
208
+ tool: {
209
+ name: "canvas_delete_file",
210
+ description: "Delete a file from Canvas",
211
+ inputSchema: {
212
+ type: "object",
213
+ properties: {
214
+ file_id: { type: "number", description: "The ID of the file to delete" }
215
+ },
216
+ required: ["file_id"],
217
+ },
218
+ },
219
+ handler: async (client, args) => {
220
+ const input = z.object({
221
+ file_id: z.number()
222
+ }).parse(args);
223
+ await client.deleteFile(input.file_id);
224
+ return {
225
+ content: [{ type: "text", text: `File ${input.file_id} deleted successfully.` }],
226
+ };
227
+ }
228
+ }
229
+ ];
@@ -0,0 +1,187 @@
1
+ import { z } from "zod";
2
+ export const gradingTools = [
3
+ {
4
+ name: "canvas_grade_submission",
5
+ tool: {
6
+ name: "canvas_grade_submission",
7
+ description: "Grade a submission for a specific student",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ course_id: { type: "number", description: "The ID of the course" },
12
+ assignment_id: { type: "number", description: "The ID of the assignment" },
13
+ student_id: { type: "number", description: "The ID of the student" },
14
+ grade: { type: "number", description: "The numeric grade to assign" },
15
+ comment: { type: "string", description: "Optional comment" },
16
+ rubric_assessment: {
17
+ type: "object",
18
+ description: "Rubric assessment data. Map of criterion ID to rating/points.",
19
+ additionalProperties: {
20
+ type: "object",
21
+ properties: {
22
+ points: { type: "number" },
23
+ rating_id: { type: "string" },
24
+ comments: { type: "string" }
25
+ }
26
+ }
27
+ }
28
+ },
29
+ required: ["course_id", "assignment_id", "student_id", "grade"],
30
+ },
31
+ },
32
+ handler: async (client, args) => {
33
+ const input = z.object({
34
+ course_id: z.coerce.number(),
35
+ assignment_id: z.coerce.number(),
36
+ student_id: z.coerce.number(),
37
+ grade: z.union([z.number(), z.string()]),
38
+ comment: z.string().optional(),
39
+ rubric_assessment: z.record(z.string(), z.object({
40
+ rating_id: z.string().optional(),
41
+ points: z.number(),
42
+ comments: z.string().optional()
43
+ })).optional()
44
+ }).parse(args);
45
+ const result = await client.gradeSubmission(input.course_id, input.assignment_id, input.student_id, input.grade, input.comment, input.rubric_assessment);
46
+ return {
47
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
48
+ };
49
+ }
50
+ },
51
+ {
52
+ name: "canvas_grade_multiple_submissions",
53
+ tool: {
54
+ name: "canvas_grade_multiple_submissions",
55
+ description: "Grade multiple submissions at once, either by providing student_ids or filtering by status (e.g. unsubmitted)",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ course_id: { type: "number", description: "The ID of the course" },
60
+ assignment_id: { type: "number", description: "The ID of the assignment" },
61
+ grade: { type: "number", description: "The grade to assign" },
62
+ comment: { type: "string", description: "Optional comment" },
63
+ student_ids: {
64
+ type: "array",
65
+ items: { type: "number" },
66
+ description: "List of student IDs to grade"
67
+ },
68
+ filter_status: {
69
+ type: "string",
70
+ enum: ["unsubmitted", "missing", "late"],
71
+ description: "Filter student submissions by status"
72
+ },
73
+ rubric_assessment: {
74
+ type: "object",
75
+ description: "Rubric assessment data. Map of criterion ID to rating/points.",
76
+ additionalProperties: {
77
+ type: "object",
78
+ properties: {
79
+ points: { type: "number" },
80
+ rating_id: { type: "string" },
81
+ comments: { type: "string" }
82
+ }
83
+ }
84
+ }
85
+ },
86
+ required: ["course_id", "assignment_id", "grade"]
87
+ },
88
+ },
89
+ handler: async (client, args) => {
90
+ const input = z.object({
91
+ course_id: z.number(),
92
+ assignment_id: z.number(),
93
+ grade: z.union([z.number(), z.string()]),
94
+ comment: z.string().optional(),
95
+ student_ids: z.array(z.number()).optional(),
96
+ filter_status: z.enum(['unsubmitted', 'missing', 'late']).optional(),
97
+ rubric_assessment: z.record(z.string(), z.object({
98
+ rating_id: z.string().optional(),
99
+ points: z.number(),
100
+ comments: z.string().optional()
101
+ })).optional()
102
+ }).parse(args);
103
+ if (!input.student_ids && !input.filter_status) {
104
+ throw new Error("You must provide either student_ids or a filter_status (e.g. 'unsubmitted')");
105
+ }
106
+ const cid = input.course_id;
107
+ const aid = input.assignment_id;
108
+ let studentsToGrade = [];
109
+ if (input.student_ids) {
110
+ studentsToGrade = input.student_ids;
111
+ }
112
+ else if (input.filter_status) {
113
+ const submissions = await client.getSubmissions(cid, aid);
114
+ const status = input.filter_status;
115
+ studentsToGrade = submissions
116
+ .filter(s => {
117
+ if (status === 'unsubmitted')
118
+ return s.workflow_state === 'unsubmitted' || !s.submitted_at;
119
+ if (status === 'missing')
120
+ return s.missing;
121
+ if (status === 'late')
122
+ return s.late;
123
+ return false;
124
+ })
125
+ .map(s => s.user_id);
126
+ }
127
+ if (studentsToGrade.length === 0) {
128
+ return { content: [{ type: "text", text: "No students found matching the criteria." }] };
129
+ }
130
+ const results = [];
131
+ for (const userId of studentsToGrade) {
132
+ try {
133
+ await client.gradeSubmission(cid, aid, userId, input.grade, input.comment, input.rubric_assessment);
134
+ results.push({ student_id: userId, status: 'graded', grade: input.grade });
135
+ }
136
+ catch (err) {
137
+ results.push({ student_id: userId, status: 'error', error: err.message });
138
+ }
139
+ }
140
+ return {
141
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
142
+ };
143
+ }
144
+ },
145
+ {
146
+ name: "canvas_audit_course",
147
+ tool: {
148
+ name: "canvas_audit_course",
149
+ description: "Audit a course for future assignments and missing submissions",
150
+ inputSchema: {
151
+ type: "object",
152
+ properties: {
153
+ course_id: { type: "number", description: "The ID of the course" },
154
+ },
155
+ required: ["course_id"],
156
+ },
157
+ },
158
+ handler: async (client, args) => {
159
+ const input = z.object({ course_id: z.coerce.number() }).parse(args);
160
+ const cid = input.course_id;
161
+ const assignments = await client.getAssignments(cid);
162
+ const now = new Date();
163
+ const future = assignments.filter(a => a.due_at && new Date(a.due_at) > now);
164
+ if (future.length === 0) {
165
+ return { content: [{ type: "text", text: "No future assignments found." }] };
166
+ }
167
+ let report = `Audit for Course ${cid}:\n`;
168
+ for (const a of future) {
169
+ if (!a.id)
170
+ continue;
171
+ const subs = await client.getSubmissions(cid, a.id);
172
+ const missing = subs.filter(s => s.workflow_state === 'unsubmitted' || !s.submitted_at);
173
+ if (missing.length > 0) {
174
+ report += `\nAssignment: ${a.name} (Due: ${a.due_at})\n`;
175
+ report += ` ${missing.length} missing submissions:\n`;
176
+ missing.forEach(m => {
177
+ const name = m.user?.name || `User ${m.user_id}`;
178
+ report += ` - ${name} (ID: ${m.user_id})\n`;
179
+ });
180
+ }
181
+ }
182
+ return {
183
+ content: [{ type: "text", text: report }],
184
+ };
185
+ }
186
+ }
187
+ ];
@@ -0,0 +1,192 @@
1
+ import { resolveCourseId, resolveStudentId } from "../common/helpers.js";
2
+ import { z } from "zod";
3
+ export const groupTools = [
4
+ {
5
+ name: "canvas_list_group_categories",
6
+ tool: {
7
+ name: "canvas_list_group_categories",
8
+ description: "List group categories (Group Sets) in a 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 categories = await client.getGroupCategories(courseId);
24
+ return {
25
+ content: [{ type: "text", text: JSON.stringify(categories, null, 2) }]
26
+ };
27
+ }
28
+ },
29
+ {
30
+ name: "canvas_create_group_category",
31
+ tool: {
32
+ name: "canvas_create_group_category",
33
+ description: "Create a new group category (Group Set) 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
+ name: { type: "string", description: "Name of the group category" },
42
+ self_signup: { type: "string", enum: ["enabled", "restricted"], description: "Allow students to sign up themselves" },
43
+ auto_leader: { type: "string", enum: ["first", "random"], description: "Auto-assign a leader" },
44
+ group_limit: { type: "number", description: "Maximum number of users in each group (requires self_signup)" },
45
+ create_group_count: { type: "number", description: "Automatically create this number of groups" },
46
+ split_group_count: { type: "number", description: "Create this many groups and randomly assign students" }
47
+ },
48
+ required: ["course_id", "name"]
49
+ }
50
+ },
51
+ handler: async (client, args) => {
52
+ const input = z.object({
53
+ course_id: z.union([z.number(), z.string()]),
54
+ name: z.string(),
55
+ self_signup: z.enum(["enabled", "restricted"]).optional(),
56
+ auto_leader: z.enum(["first", "random"]).optional(),
57
+ group_limit: z.number().optional(),
58
+ create_group_count: z.number().optional(),
59
+ split_group_count: z.number().optional()
60
+ }).parse(args);
61
+ const courseId = await resolveCourseId(client, input.course_id);
62
+ const category = await client.createGroupCategory(courseId, {
63
+ name: input.name,
64
+ self_signup: input.self_signup,
65
+ auto_leader: input.auto_leader,
66
+ group_limit: input.group_limit,
67
+ create_group_count: input.create_group_count,
68
+ split_group_count: input.split_group_count
69
+ });
70
+ return {
71
+ content: [{ type: "text", text: JSON.stringify(category, null, 2) }]
72
+ };
73
+ }
74
+ },
75
+ {
76
+ name: "canvas_create_group",
77
+ tool: {
78
+ name: "canvas_create_group",
79
+ description: "Create a new group within a group category",
80
+ inputSchema: {
81
+ type: "object",
82
+ properties: {
83
+ group_category_id: { type: "number", description: "The ID of the group category" },
84
+ name: { type: "string", description: "Name of the group" },
85
+ description: { type: "string", description: "Description of the group" },
86
+ join_level: { type: "string", enum: ["parent_context_auto_join", "parent_context_request", "invitation_only"], description: "How people can join" },
87
+ is_public: { type: "boolean", description: "Whether the group is public" }
88
+ },
89
+ required: ["group_category_id", "name"]
90
+ }
91
+ },
92
+ handler: async (client, args) => {
93
+ const input = z.object({
94
+ group_category_id: z.number(),
95
+ name: z.string(),
96
+ description: z.string().optional(),
97
+ join_level: z.enum(["parent_context_auto_join", "parent_context_request", "invitation_only"]).optional(),
98
+ is_public: z.boolean().optional()
99
+ }).parse(args);
100
+ const group = await client.createGroup(input.group_category_id, {
101
+ name: input.name,
102
+ description: input.description,
103
+ join_level: input.join_level,
104
+ is_public: input.is_public
105
+ });
106
+ return {
107
+ content: [{ type: "text", text: JSON.stringify(group, null, 2) }]
108
+ };
109
+ }
110
+ },
111
+ {
112
+ name: "canvas_list_groups_in_category",
113
+ tool: {
114
+ name: "canvas_list_groups_in_category",
115
+ description: "List groups inside a group category",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {
119
+ group_category_id: { type: "number", description: "The ID of the group category" }
120
+ },
121
+ required: ["group_category_id"]
122
+ }
123
+ },
124
+ handler: async (client, args) => {
125
+ const input = z.object({ group_category_id: z.number() }).parse(args);
126
+ const groups = await client.getGroupsInCategory(input.group_category_id);
127
+ return {
128
+ content: [{ type: "text", text: JSON.stringify(groups, null, 2) }]
129
+ };
130
+ }
131
+ },
132
+ {
133
+ name: "canvas_assign_unassigned_members",
134
+ tool: {
135
+ name: "canvas_assign_unassigned_members",
136
+ description: "Randomly assign unassigned students to existing groups in a category",
137
+ inputSchema: {
138
+ type: "object",
139
+ properties: {
140
+ group_category_id: { type: "number", description: "The ID of the group category" },
141
+ sync: { type: "boolean", description: "Run assignment synchronously" }
142
+ },
143
+ required: ["group_category_id"]
144
+ }
145
+ },
146
+ handler: async (client, args) => {
147
+ const input = z.object({
148
+ group_category_id: z.number(),
149
+ sync: z.boolean().optional().default(false)
150
+ }).parse(args);
151
+ const result = await client.assignUnassignedMembers(input.group_category_id, input.sync);
152
+ return {
153
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
154
+ };
155
+ }
156
+ },
157
+ {
158
+ name: "canvas_add_group_member",
159
+ tool: {
160
+ name: "canvas_add_group_member",
161
+ description: "Add a student to a group",
162
+ inputSchema: {
163
+ type: "object",
164
+ properties: {
165
+ course_id: {
166
+ anyOf: [{ type: "number" }, { type: "string" }],
167
+ description: "The ID or name of the course"
168
+ },
169
+ group_id: { type: "number", description: "The ID of the group" },
170
+ student_id: {
171
+ anyOf: [{ type: "number" }, { type: "string" }],
172
+ description: "The ID or name of the student"
173
+ }
174
+ },
175
+ required: ["course_id", "group_id", "student_id"]
176
+ }
177
+ },
178
+ handler: async (client, args) => {
179
+ const input = z.object({
180
+ course_id: z.union([z.number(), z.string()]),
181
+ group_id: z.number(),
182
+ student_id: z.union([z.number(), z.string()])
183
+ }).parse(args);
184
+ const courseId = await resolveCourseId(client, input.course_id);
185
+ const studentId = await resolveStudentId(client, courseId, input.student_id);
186
+ const membership = await client.addGroupMember(input.group_id, studentId);
187
+ return {
188
+ content: [{ type: "text", text: JSON.stringify(membership, null, 2) }]
189
+ };
190
+ }
191
+ }
192
+ ];