canvas-agent 1.0.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.
Files changed (40) hide show
  1. package/README.md +41 -0
  2. package/dist/canvas-client.d.ts +24 -0
  3. package/dist/canvas-client.js +90 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +10 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +41 -0
  8. package/dist/setup.d.ts +6 -0
  9. package/dist/setup.js +287 -0
  10. package/dist/tools/analytics.d.ts +2 -0
  11. package/dist/tools/analytics.js +69 -0
  12. package/dist/tools/assignments.d.ts +2 -0
  13. package/dist/tools/assignments.js +175 -0
  14. package/dist/tools/calendar.d.ts +2 -0
  15. package/dist/tools/calendar.js +119 -0
  16. package/dist/tools/courses.d.ts +2 -0
  17. package/dist/tools/courses.js +52 -0
  18. package/dist/tools/discussions.d.ts +2 -0
  19. package/dist/tools/discussions.js +134 -0
  20. package/dist/tools/enrollments.d.ts +2 -0
  21. package/dist/tools/enrollments.js +105 -0
  22. package/dist/tools/files.d.ts +2 -0
  23. package/dist/tools/files.js +148 -0
  24. package/dist/tools/grading.d.ts +2 -0
  25. package/dist/tools/grading.js +260 -0
  26. package/dist/tools/modules.d.ts +2 -0
  27. package/dist/tools/modules.js +215 -0
  28. package/dist/tools/new-quizzes.d.ts +2 -0
  29. package/dist/tools/new-quizzes.js +444 -0
  30. package/dist/tools/pages.d.ts +2 -0
  31. package/dist/tools/pages.js +150 -0
  32. package/dist/tools/quizzes.d.ts +2 -0
  33. package/dist/tools/quizzes.js +83 -0
  34. package/dist/tools/rubrics.d.ts +2 -0
  35. package/dist/tools/rubrics.js +298 -0
  36. package/dist/tools/scheduling.d.ts +2 -0
  37. package/dist/tools/scheduling.js +133 -0
  38. package/dist/tools/submissions.d.ts +2 -0
  39. package/dist/tools/submissions.js +150 -0
  40. package/package.json +43 -0
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+ import { canvas, canvasAll } from "../canvas-client.js";
3
+ export function registerFileTools(server) {
4
+ server.tool("list_course_files", "List files in a course, optionally filtered by search term, content type, or sort order.", {
5
+ course_id: z.string().describe("Canvas course ID"),
6
+ search_term: z.string().optional().describe("Filter by filename substring"),
7
+ content_types: z
8
+ .array(z.string())
9
+ .optional()
10
+ .describe('Filter by MIME type (e.g. ["application/pdf"])'),
11
+ sort: z
12
+ .enum(["name", "size", "created_at", "updated_at"])
13
+ .optional()
14
+ .describe("Sort order"),
15
+ }, async ({ course_id, search_term, content_types, sort }) => {
16
+ const params = {};
17
+ if (search_term)
18
+ params.search_term = search_term;
19
+ if (sort)
20
+ params.sort = sort;
21
+ // Canvas expects content_types[] as repeated params; encode manually
22
+ let extraQs = "";
23
+ if (content_types && content_types.length > 0) {
24
+ extraQs = content_types
25
+ .map((ct) => `content_types[]=${encodeURIComponent(ct)}`)
26
+ .join("&");
27
+ }
28
+ const path = extraQs
29
+ ? `/courses/${course_id}/files?${extraQs}`
30
+ : `/courses/${course_id}/files`;
31
+ const files = await canvasAll(path, params);
32
+ const summary = files.map((f) => ({
33
+ id: f.id,
34
+ display_name: f.display_name,
35
+ filename: f.filename,
36
+ size: f.size,
37
+ content_type: f["content-type"],
38
+ url: f.url,
39
+ created_at: f.created_at,
40
+ updated_at: f.updated_at,
41
+ folder_id: f.folder_id,
42
+ }));
43
+ return {
44
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
45
+ };
46
+ });
47
+ server.tool("get_file", "Get full details of a single file, including its download URL.", {
48
+ file_id: z.string().describe("File ID"),
49
+ }, async ({ file_id }) => {
50
+ const file = await canvas(`/files/${file_id}`);
51
+ return {
52
+ content: [{ type: "text", text: JSON.stringify(file, null, 2) }],
53
+ };
54
+ });
55
+ server.tool("list_folders", "List all folders in a course.", {
56
+ course_id: z.string().describe("Canvas course ID"),
57
+ }, async ({ course_id }) => {
58
+ const folders = await canvasAll(`/courses/${course_id}/folders`);
59
+ const summary = folders.map((f) => ({
60
+ id: f.id,
61
+ name: f.name,
62
+ full_name: f.full_name,
63
+ parent_folder_id: f.parent_folder_id,
64
+ files_count: f.files_count,
65
+ folders_count: f.folders_count,
66
+ }));
67
+ return {
68
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
69
+ };
70
+ });
71
+ server.tool("create_folder", "Create a new folder in a course.", {
72
+ course_id: z.string().describe("Canvas course ID"),
73
+ name: z.string().describe("Folder name"),
74
+ parent_folder_path: z
75
+ .string()
76
+ .optional()
77
+ .default("/")
78
+ .describe("Parent folder path (default: root)"),
79
+ hidden: z.boolean().optional().describe("Hide the folder from students"),
80
+ locked: z.boolean().optional().describe("Lock the folder"),
81
+ }, async ({ course_id, name, parent_folder_path, hidden, locked }) => {
82
+ const body = { name, parent_folder_path };
83
+ if (hidden !== undefined)
84
+ body.hidden = hidden;
85
+ if (locked !== undefined)
86
+ body.locked = locked;
87
+ const result = await canvas(`/courses/${course_id}/folders`, {
88
+ method: "POST",
89
+ body: JSON.stringify(body),
90
+ });
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: `Created folder "${result.name}" (ID: ${result.id}) at ${result.full_name}`,
96
+ },
97
+ ],
98
+ };
99
+ });
100
+ server.tool("delete_file", "Permanently delete a file.", {
101
+ file_id: z.string().describe("File ID"),
102
+ }, async ({ file_id }) => {
103
+ await canvas(`/files/${file_id}`, { method: "DELETE" });
104
+ return {
105
+ content: [{ type: "text", text: `Deleted file ${file_id}` }],
106
+ };
107
+ });
108
+ server.tool("update_file", "Update file metadata. Only include fields you want to change.", {
109
+ file_id: z.string().describe("File ID"),
110
+ name: z.string().optional().describe("Rename the file"),
111
+ lock_at: z.string().optional().describe("Lock date (ISO 8601)"),
112
+ unlock_at: z.string().optional().describe("Unlock date (ISO 8601)"),
113
+ locked: z.boolean().optional().describe("Lock the file"),
114
+ hidden: z.boolean().optional().describe("Hide from students"),
115
+ }, async ({ file_id, ...params }) => {
116
+ const result = await canvas(`/files/${file_id}`, {
117
+ method: "PUT",
118
+ body: JSON.stringify(params),
119
+ });
120
+ return {
121
+ content: [
122
+ {
123
+ type: "text",
124
+ text: `Updated file "${result.display_name}" (ID: ${result.id})`,
125
+ },
126
+ ],
127
+ };
128
+ });
129
+ server.tool("get_file_quota", "Get the file storage quota and usage for a course.", {
130
+ course_id: z.string().describe("Canvas course ID"),
131
+ }, async ({ course_id }) => {
132
+ const quota = await canvas(`/courses/${course_id}/files/quota`);
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: JSON.stringify({
138
+ quota: quota.quota,
139
+ quota_used: quota.quota_used,
140
+ quota_remaining: quota.quota - quota.quota_used,
141
+ quota_human: `${(quota.quota / 1024 / 1024).toFixed(1)} MB`,
142
+ used_human: `${(quota.quota_used / 1024 / 1024).toFixed(1)} MB`,
143
+ }, null, 2),
144
+ },
145
+ ],
146
+ };
147
+ });
148
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGradingTools(server: McpServer): void;
@@ -0,0 +1,260 @@
1
+ import { z } from "zod";
2
+ import { canvas, canvasAll } from "../canvas-client.js";
3
+ export function registerGradingTools(server) {
4
+ server.tool("grade_submission", "Grade a single student submission with a score and optional comment. For rubric-based grading, use grade_with_rubric instead. Works for regular assignments, graded discussions (use the assignment_id), and New Quizzes (use the assignment_id).", {
5
+ course_id: z.string().describe("Canvas course ID"),
6
+ assignment_id: z.string().describe("Assignment ID (also works for graded discussions and New Quizzes via their linked assignment_id)"),
7
+ student_id: z.string().describe("Student user ID (use list_students or list_gradeable_students to find IDs)"),
8
+ score: z.union([z.number(), z.string()]).optional().describe("Grade to assign: points (95), percentage ('85%'), letter grade ('A-'), 'pass'/'fail', or 'complete'/'incomplete'"),
9
+ excuse: z
10
+ .boolean()
11
+ .optional()
12
+ .describe("Excuse the student from the assignment"),
13
+ late_policy_status: z
14
+ .enum(["none", "missing", "late", "extended"])
15
+ .optional()
16
+ .describe("Override the late policy status"),
17
+ comment: z
18
+ .string()
19
+ .optional()
20
+ .describe("Text comment on the submission"),
21
+ comment_file_ids: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("File IDs to attach to the comment"),
25
+ }, async ({ course_id, assignment_id, student_id, score, excuse, late_policy_status, comment, comment_file_ids, }) => {
26
+ const body = {};
27
+ const submission = {};
28
+ if (score !== undefined)
29
+ submission.posted_grade = score;
30
+ if (excuse !== undefined)
31
+ submission.excuse = excuse;
32
+ if (late_policy_status !== undefined)
33
+ submission.late_policy_status = late_policy_status;
34
+ if (Object.keys(submission).length > 0)
35
+ body.submission = submission;
36
+ const commentObj = {};
37
+ if (comment !== undefined)
38
+ commentObj.text_comment = comment;
39
+ if (comment_file_ids !== undefined)
40
+ commentObj.file_ids = comment_file_ids;
41
+ if (Object.keys(commentObj).length > 0)
42
+ body.comment = commentObj;
43
+ const result = await canvas(`/courses/${course_id}/assignments/${assignment_id}/submissions/${student_id}`, {
44
+ method: "PUT",
45
+ body: JSON.stringify(body),
46
+ });
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: `Graded submission for user ${student_id}: score=${result.score}, grade="${result.grade}", workflow_state=${result.workflow_state}`,
52
+ },
53
+ ],
54
+ };
55
+ });
56
+ server.tool("bulk_grade", "Grade multiple students on one assignment at once. Returns a Progress object — the grading happens asynchronously.", {
57
+ course_id: z.string().describe("Canvas course ID"),
58
+ assignment_id: z.string().describe("Assignment ID"),
59
+ grades: z
60
+ .array(z.object({
61
+ student_id: z.string().describe("Student (user) ID"),
62
+ score: z.union([z.number(), z.string()]).describe("Grade: points (95), percentage ('85%'), or letter ('A-')"),
63
+ comment: z
64
+ .string()
65
+ .optional()
66
+ .describe("Optional text comment"),
67
+ }))
68
+ .describe("Array of student grades to apply"),
69
+ }, async ({ course_id, assignment_id, grades }) => {
70
+ const grade_data = {};
71
+ for (const g of grades) {
72
+ const entry = { posted_grade: g.score };
73
+ if (g.comment !== undefined)
74
+ entry.text_comment = g.comment;
75
+ grade_data[g.student_id] = entry;
76
+ }
77
+ const result = await canvas(`/courses/${course_id}/assignments/${assignment_id}/submissions/update_grades`, {
78
+ method: "POST",
79
+ body: JSON.stringify({ grade_data }),
80
+ });
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: `Bulk grade started. Progress: id=${result.id}, workflow_state=${result.workflow_state}, url=${result.url ?? "N/A"}\n${JSON.stringify(result, null, 2)}`,
86
+ },
87
+ ],
88
+ };
89
+ });
90
+ server.tool("submission_summary", "Quick overview of submission status for an assignment — counts of graded, ungraded, and not submitted. For individual submission details, use list_submissions instead.", {
91
+ course_id: z.string().describe("Canvas course ID"),
92
+ assignment_id: z.string().describe("Assignment ID"),
93
+ }, async ({ course_id, assignment_id }) => {
94
+ const summary = await canvas(`/courses/${course_id}/assignments/${assignment_id}/submission_summary`);
95
+ return {
96
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
97
+ };
98
+ });
99
+ server.tool("list_missing_submissions", "List all missing assignments for a specific student. Very useful for parent conferences and progress reports.", {
100
+ student_id: z.string().describe("Student (user) ID"),
101
+ course_id: z
102
+ .string()
103
+ .optional()
104
+ .describe("Filter to a specific course"),
105
+ include_planner_overrides: z
106
+ .boolean()
107
+ .optional()
108
+ .describe("Include planner override info"),
109
+ }, async ({ student_id, course_id, include_planner_overrides }) => {
110
+ const params = {};
111
+ if (course_id)
112
+ params.course_id = course_id;
113
+ if (include_planner_overrides)
114
+ params["include[]"] = "planner_overrides";
115
+ const submissions = await canvasAll(`/users/${student_id}/missing_submissions`, params);
116
+ const summary = submissions.map((s) => ({
117
+ id: s.id,
118
+ name: s.name,
119
+ course_id: s.course_id,
120
+ due_at: s.due_at,
121
+ points_possible: s.points_possible,
122
+ html_url: s.html_url,
123
+ }));
124
+ return {
125
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
126
+ };
127
+ });
128
+ server.tool("list_gradeable_students", "List students who can be graded for a specific assignment. Use the returned IDs as student_id in grade_submission or grade_with_rubric.", {
129
+ course_id: z.string().describe("Canvas course ID"),
130
+ assignment_id: z.string().describe("Assignment ID"),
131
+ }, async ({ course_id, assignment_id }) => {
132
+ const students = await canvasAll(`/courses/${course_id}/assignments/${assignment_id}/gradeable_students`);
133
+ const summary = students.map((s) => ({
134
+ id: s.id,
135
+ display_name: s.display_name,
136
+ avatar_url: s.avatar_url,
137
+ }));
138
+ return {
139
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
140
+ };
141
+ });
142
+ server.tool("get_late_policy", "Get the late policy configuration for a course.", {
143
+ course_id: z.string().describe("Canvas course ID"),
144
+ }, async ({ course_id }) => {
145
+ const result = await canvas(`/courses/${course_id}/late_policy`);
146
+ return {
147
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
148
+ };
149
+ });
150
+ server.tool("set_late_policy", "Create or update the late policy for a course. Only include fields you want to change.", {
151
+ course_id: z.string().describe("Canvas course ID"),
152
+ missing_submission_deduction_enabled: z
153
+ .boolean()
154
+ .optional()
155
+ .describe("Enable automatic deduction for missing submissions"),
156
+ missing_submission_deduction: z
157
+ .number()
158
+ .optional()
159
+ .describe("Percentage to deduct for missing submissions"),
160
+ late_submission_deduction_enabled: z
161
+ .boolean()
162
+ .optional()
163
+ .describe("Enable automatic deduction for late submissions"),
164
+ late_submission_deduction: z
165
+ .number()
166
+ .optional()
167
+ .describe("Percentage to deduct per period for late submissions"),
168
+ late_submission_interval: z
169
+ .enum(["day", "hour"])
170
+ .optional()
171
+ .describe("Interval for late deduction (day or hour)"),
172
+ late_submission_minimum_percent_enabled: z
173
+ .boolean()
174
+ .optional()
175
+ .describe("Enable a minimum grade for late submissions"),
176
+ late_submission_minimum_percent: z
177
+ .number()
178
+ .optional()
179
+ .describe("Minimum percentage a late submission can receive"),
180
+ }, async ({ course_id, ...policyFields }) => {
181
+ // Filter out undefined values
182
+ const late_policy = {};
183
+ for (const [key, value] of Object.entries(policyFields)) {
184
+ if (value !== undefined)
185
+ late_policy[key] = value;
186
+ }
187
+ const result = await canvas(`/courses/${course_id}/late_policy`, {
188
+ method: "PATCH",
189
+ body: JSON.stringify({ late_policy }),
190
+ });
191
+ return {
192
+ content: [
193
+ {
194
+ type: "text",
195
+ text: `Late policy updated.\n${JSON.stringify(result, null, 2)}`,
196
+ },
197
+ ],
198
+ };
199
+ });
200
+ server.tool("post_grades", "Post (make visible) grades for an assignment so students can see them. Optionally post for specific students only.", {
201
+ course_id: z.string().describe("Canvas course ID"),
202
+ assignment_id: z.string().describe("Assignment ID"),
203
+ only_student_ids: z
204
+ .array(z.string())
205
+ .optional()
206
+ .describe("Post grades only for these student IDs (omit to post all)"),
207
+ }, async ({ course_id, assignment_id, only_student_ids }) => {
208
+ const body = {};
209
+ if (only_student_ids !== undefined) {
210
+ body.student_ids = only_student_ids;
211
+ }
212
+ const result = await canvas(`/courses/${course_id}/assignments/${assignment_id}/post_grades`, {
213
+ method: "POST",
214
+ body: JSON.stringify(body),
215
+ });
216
+ return {
217
+ content: [
218
+ {
219
+ type: "text",
220
+ text: `Grades posted for assignment ${assignment_id}.\n${JSON.stringify(result, null, 2)}`,
221
+ },
222
+ ],
223
+ };
224
+ });
225
+ server.tool("hide_grades", "Hide grades from students for an assignment. Optionally hide for specific students only.", {
226
+ course_id: z.string().describe("Canvas course ID"),
227
+ assignment_id: z.string().describe("Assignment ID"),
228
+ only_student_ids: z
229
+ .array(z.string())
230
+ .optional()
231
+ .describe("Hide grades only for these student IDs (omit to hide all)"),
232
+ }, async ({ course_id, assignment_id, only_student_ids }) => {
233
+ const body = {};
234
+ if (only_student_ids !== undefined) {
235
+ body.student_ids = only_student_ids;
236
+ }
237
+ const result = await canvas(`/courses/${course_id}/assignments/${assignment_id}/hide_grades`, {
238
+ method: "POST",
239
+ body: JSON.stringify(body),
240
+ });
241
+ return {
242
+ content: [
243
+ {
244
+ type: "text",
245
+ text: `Grades hidden for assignment ${assignment_id}.\n${JSON.stringify(result, null, 2)}`,
246
+ },
247
+ ],
248
+ };
249
+ });
250
+ server.tool("get_grading_standards", "List grading scales/standards defined for a course (e.g. A/B/C letter grades with cutoffs).", {
251
+ course_id: z.string().describe("Canvas course ID"),
252
+ }, async ({ course_id }) => {
253
+ const standards = await canvasAll(`/courses/${course_id}/grading_standards`);
254
+ return {
255
+ content: [
256
+ { type: "text", text: JSON.stringify(standards, null, 2) },
257
+ ],
258
+ };
259
+ });
260
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerModuleTools(server: McpServer): void;
@@ -0,0 +1,215 @@
1
+ import { z } from "zod";
2
+ import { canvas, canvasAll } from "../canvas-client.js";
3
+ export function registerModuleTools(server) {
4
+ server.tool("create_module", "Create a new module in a course. Use list_modules to see existing modules first.", {
5
+ course_id: z.string().describe("Canvas course ID"),
6
+ name: z.string().describe("Module name"),
7
+ position: z.number().optional().describe("Position in the module list"),
8
+ unlock_at: z
9
+ .string()
10
+ .optional()
11
+ .describe("Date the module unlocks (ISO 8601)"),
12
+ require_sequential_progress: z
13
+ .boolean()
14
+ .optional()
15
+ .describe("Require students to complete items in order"),
16
+ prerequisite_module_ids: z
17
+ .array(z.string())
18
+ .optional()
19
+ .describe("IDs of modules that must be completed first"),
20
+ }, async ({ course_id, name, position, unlock_at, require_sequential_progress, prerequisite_module_ids }) => {
21
+ const moduleData = { name };
22
+ if (position !== undefined)
23
+ moduleData.position = position;
24
+ if (unlock_at !== undefined)
25
+ moduleData.unlock_at = unlock_at;
26
+ if (require_sequential_progress !== undefined)
27
+ moduleData.require_sequential_progress = require_sequential_progress;
28
+ if (prerequisite_module_ids !== undefined)
29
+ moduleData.prerequisite_module_ids = prerequisite_module_ids;
30
+ const result = await canvas(`/courses/${course_id}/modules`, {
31
+ method: "POST",
32
+ body: JSON.stringify({ module: moduleData }),
33
+ });
34
+ return {
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: `Created module "${result.name}" (ID: ${result.id})`,
39
+ },
40
+ ],
41
+ };
42
+ });
43
+ server.tool("update_module", "Update an existing module. Only include fields you want to change.", {
44
+ course_id: z.string().describe("Canvas course ID"),
45
+ module_id: z.string().describe("Module ID"),
46
+ name: z.string().optional(),
47
+ position: z.number().optional(),
48
+ published: z.boolean().optional(),
49
+ unlock_at: z.string().optional().describe("ISO 8601"),
50
+ }, async ({ course_id, module_id, ...params }) => {
51
+ const result = await canvas(`/courses/${course_id}/modules/${module_id}`, {
52
+ method: "PUT",
53
+ body: JSON.stringify({ module: params }),
54
+ });
55
+ return {
56
+ content: [
57
+ {
58
+ type: "text",
59
+ text: `Updated module "${result.name}" (ID: ${result.id})`,
60
+ },
61
+ ],
62
+ };
63
+ });
64
+ server.tool("delete_module", "Delete a module from a course.", {
65
+ course_id: z.string().describe("Canvas course ID"),
66
+ module_id: z.string().describe("Module ID"),
67
+ }, async ({ course_id, module_id }) => {
68
+ await canvas(`/courses/${course_id}/modules/${module_id}`, {
69
+ method: "DELETE",
70
+ });
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text",
75
+ text: `Deleted module ${module_id}`,
76
+ },
77
+ ],
78
+ };
79
+ });
80
+ server.tool("list_module_items", "List all items in a module.", {
81
+ course_id: z.string().describe("Canvas course ID"),
82
+ module_id: z.string().describe("Module ID"),
83
+ }, async ({ course_id, module_id }) => {
84
+ const items = await canvasAll(`/courses/${course_id}/modules/${module_id}/items`);
85
+ const summary = items.map((i) => ({
86
+ id: i.id,
87
+ title: i.title,
88
+ type: i.type,
89
+ position: i.position,
90
+ content_id: i.content_id,
91
+ html_url: i.html_url,
92
+ published: i.published,
93
+ }));
94
+ return {
95
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
96
+ };
97
+ });
98
+ server.tool("add_module_item", "Add an item to a module. Required fields depend on type: Assignment/Discussion/File need content_id, Page needs page_url, SubHeader/ExternalUrl need title. For New Quizzes, use type='Assignment' with the New Quiz's assignment_id (not type='Quiz'). type='Quiz' is for Classic Quizzes only.", {
99
+ course_id: z.string().describe("Canvas course ID"),
100
+ module_id: z.string().describe("Module ID"),
101
+ type: z
102
+ .enum([
103
+ "Assignment",
104
+ "Quiz",
105
+ "Discussion",
106
+ "File",
107
+ "Page",
108
+ "SubHeader",
109
+ "ExternalUrl",
110
+ "ExternalTool",
111
+ ])
112
+ .describe("Type of item to add"),
113
+ content_id: z
114
+ .string()
115
+ .optional()
116
+ .describe("ID of the content (required for Assignment, Quiz, Discussion, File)"),
117
+ page_url: z
118
+ .string()
119
+ .optional()
120
+ .describe("URL slug of the page (required for Page type)"),
121
+ title: z
122
+ .string()
123
+ .optional()
124
+ .describe("Title (required for SubHeader and ExternalUrl)"),
125
+ external_url: z
126
+ .string()
127
+ .optional()
128
+ .describe("URL for ExternalUrl type"),
129
+ position: z.number().optional().describe("Position within the module"),
130
+ indent: z
131
+ .number()
132
+ .optional()
133
+ .describe("Indentation level (0-5)"),
134
+ }, async ({ course_id, module_id, type, content_id, page_url, title, external_url, position, indent }) => {
135
+ const moduleItem = { type };
136
+ if (content_id !== undefined)
137
+ moduleItem.content_id = content_id;
138
+ if (page_url !== undefined)
139
+ moduleItem.page_url = page_url;
140
+ if (title !== undefined)
141
+ moduleItem.title = title;
142
+ if (external_url !== undefined)
143
+ moduleItem.external_url = external_url;
144
+ if (position !== undefined)
145
+ moduleItem.position = position;
146
+ if (indent !== undefined)
147
+ moduleItem.indent = indent;
148
+ const result = await canvas(`/courses/${course_id}/modules/${module_id}/items`, {
149
+ method: "POST",
150
+ body: JSON.stringify({ module_item: moduleItem }),
151
+ });
152
+ return {
153
+ content: [
154
+ {
155
+ type: "text",
156
+ text: `Added "${result.title}" (ID: ${result.id}, type: ${result.type}) to module`,
157
+ },
158
+ ],
159
+ };
160
+ });
161
+ server.tool("update_module_item", "Update a module item. Only include fields you want to change.", {
162
+ course_id: z.string().describe("Canvas course ID"),
163
+ module_id: z.string().describe("Module ID"),
164
+ item_id: z.string().describe("Module item ID"),
165
+ title: z.string().optional(),
166
+ position: z.number().optional(),
167
+ indent: z.number().optional().describe("Indentation level (0-5)"),
168
+ published: z.boolean().optional(),
169
+ }, async ({ course_id, module_id, item_id, ...params }) => {
170
+ const result = await canvas(`/courses/${course_id}/modules/${module_id}/items/${item_id}`, {
171
+ method: "PUT",
172
+ body: JSON.stringify({ module_item: params }),
173
+ });
174
+ return {
175
+ content: [
176
+ {
177
+ type: "text",
178
+ text: `Updated module item "${result.title}" (ID: ${result.id})`,
179
+ },
180
+ ],
181
+ };
182
+ });
183
+ server.tool("delete_module_item", "Delete an item from a module.", {
184
+ course_id: z.string().describe("Canvas course ID"),
185
+ module_id: z.string().describe("Module ID"),
186
+ item_id: z.string().describe("Module item ID"),
187
+ }, async ({ course_id, module_id, item_id }) => {
188
+ await canvas(`/courses/${course_id}/modules/${module_id}/items/${item_id}`, { method: "DELETE" });
189
+ return {
190
+ content: [
191
+ {
192
+ type: "text",
193
+ text: `Deleted module item ${item_id}`,
194
+ },
195
+ ],
196
+ };
197
+ });
198
+ server.tool("publish_module", "Publish a module (makes it visible to students). Note: this publishes the module container only — individual items retain their own published state.", {
199
+ course_id: z.string().describe("Canvas course ID"),
200
+ module_id: z.string().describe("Module ID"),
201
+ }, async ({ course_id, module_id }) => {
202
+ const result = await canvas(`/courses/${course_id}/modules/${module_id}`, {
203
+ method: "PUT",
204
+ body: JSON.stringify({ module: { published: true } }),
205
+ });
206
+ return {
207
+ content: [
208
+ {
209
+ type: "text",
210
+ text: `Published module "${result.name}" (ID: ${result.id})`,
211
+ },
212
+ ],
213
+ };
214
+ });
215
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerNewQuizTools(server: McpServer): void;