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,444 @@
1
+ import { z } from "zod";
2
+ import { canvas, canvasAll } from "../canvas-client.js";
3
+ // New Quizzes use /api/quiz/v1/ instead of /api/v1/
4
+ // Since canvas() and canvasAll() prepend BASE_URL (/api/v1),
5
+ // we use a relative path trick: /api/v1/../quiz/v1 resolves to /api/quiz/v1
6
+ function quizApiPath(path) {
7
+ return `/../quiz/v1${path}`;
8
+ }
9
+ export function registerNewQuizTools(server) {
10
+ // ── 1. Get New Quiz Details ────────────────────────────────────────────
11
+ server.tool("get_new_quiz", "Get full details of a single New Quiz (Quizzes.Next) including settings, time limits, and configuration. The assignment_id is the Canvas assignment ID (found via list_new_quizzes or list_assignments where is_quiz_lti_assignment=true).", {
12
+ course_id: z.string().describe("Canvas course ID"),
13
+ assignment_id: z.string().describe("The assignment ID of the New Quiz"),
14
+ }, async ({ course_id, assignment_id }) => {
15
+ const quiz = await canvas(quizApiPath(`/courses/${course_id}/quizzes/${assignment_id}`));
16
+ return {
17
+ content: [{ type: "text", text: JSON.stringify(quiz, null, 2) }],
18
+ };
19
+ });
20
+ // ── 2. Create New Quiz ─────────────────────────────────────────────────
21
+ server.tool("create_new_quiz", "Create a New Quiz (Quizzes.Next) in a course. Returns the created quiz object including its assignment_id.", {
22
+ course_id: z.string().describe("Canvas course ID"),
23
+ title: z.string().describe("Quiz title"),
24
+ assignment_group_id: z
25
+ .string()
26
+ .optional()
27
+ .describe("Assignment group ID to place the quiz in"),
28
+ points_possible: z.number().optional().describe("Total point value"),
29
+ due_at: z.string().optional().describe("Due date (ISO 8601)"),
30
+ published: z.boolean().optional().describe("Publish immediately"),
31
+ }, async ({ course_id, ...params }) => {
32
+ const result = await canvas(quizApiPath(`/courses/${course_id}/quizzes`), {
33
+ method: "POST",
34
+ body: JSON.stringify({ quiz: params }),
35
+ });
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: `Created New Quiz "${result.title}" (ID: ${result.id})\n${JSON.stringify(result, null, 2)}`,
41
+ },
42
+ ],
43
+ };
44
+ });
45
+ // ── 3. Update New Quiz ─────────────────────────────────────────────────
46
+ server.tool("update_new_quiz", "Update a New Quiz's settings (time limit, shuffle, attempts, etc.). For date changes (due_at, unlock_at, lock_at), use update_assignment_dates with the assignment_id instead — New Quizzes are assignments.", {
47
+ course_id: z.string().describe("Canvas course ID"),
48
+ assignment_id: z.string().describe("The assignment ID of the New Quiz"),
49
+ title: z.string().optional().describe("Quiz title"),
50
+ points_possible: z.number().optional().describe("Total point value"),
51
+ time_limit_seconds: z
52
+ .number()
53
+ .optional()
54
+ .describe("Time limit in seconds (e.g. 3600 for 1 hour)"),
55
+ shuffle_questions: z
56
+ .boolean()
57
+ .optional()
58
+ .describe("Randomize question order"),
59
+ shuffle_answers: z
60
+ .boolean()
61
+ .optional()
62
+ .describe("Randomize answer choice order"),
63
+ allow_backtracking: z
64
+ .boolean()
65
+ .optional()
66
+ .describe("Allow students to go back to previous questions"),
67
+ multiple_attempts_enabled: z
68
+ .boolean()
69
+ .optional()
70
+ .describe("Allow multiple attempts"),
71
+ max_attempts: z
72
+ .number()
73
+ .optional()
74
+ .describe("Maximum number of attempts (requires multiple_attempts_enabled)"),
75
+ published: z.boolean().optional().describe("Publish or unpublish"),
76
+ }, async ({ course_id, assignment_id, ...params }) => {
77
+ const result = await canvas(quizApiPath(`/courses/${course_id}/quizzes/${assignment_id}`), {
78
+ method: "PATCH",
79
+ body: JSON.stringify({ quiz: params }),
80
+ });
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: `Updated New Quiz "${result.title}" (ID: ${result.id})\n${JSON.stringify(result, null, 2)}`,
86
+ },
87
+ ],
88
+ };
89
+ });
90
+ // ── 4. List Quiz Items (Questions) ─────────────────────────────────────
91
+ server.tool("list_quiz_items", "List all items (questions) in a New Quiz. This is for New Quizzes only — Classic Quizzes do not have a question API in this server. Returns each item's ID, position, points, question type, and content.", {
92
+ course_id: z.string().describe("Canvas course ID"),
93
+ assignment_id: z.string().describe("The assignment ID of the New Quiz"),
94
+ }, async ({ course_id, assignment_id }) => {
95
+ const items = await canvasAll(quizApiPath(`/courses/${course_id}/quizzes/${assignment_id}/items`));
96
+ return {
97
+ content: [{ type: "text", text: JSON.stringify(items, null, 2) }],
98
+ };
99
+ });
100
+ // ── 5. Get Quiz Item ───────────────────────────────────────────────────
101
+ server.tool("get_quiz_item", "Get full details of a single quiz item (question), including its interaction_data, scoring_data, and feedback.", {
102
+ course_id: z.string().describe("Canvas course ID"),
103
+ assignment_id: z.string().describe("The assignment ID of the New Quiz"),
104
+ item_id: z.string().describe("The quiz item ID"),
105
+ }, async ({ course_id, assignment_id, item_id }) => {
106
+ const item = await canvas(quizApiPath(`/courses/${course_id}/quizzes/${assignment_id}/items/${item_id}`));
107
+ return {
108
+ content: [{ type: "text", text: JSON.stringify(item, null, 2) }],
109
+ };
110
+ });
111
+ // ── 6. Create Quiz Item (Question) ─────────────────────────────────────
112
+ server.tool("create_quiz_item", `Create a question (item) in a New Quiz. This is the primary tool for adding questions.
113
+
114
+ INTERACTION TYPES AND THEIR DATA FORMATS:
115
+
116
+ 1. "choice" (Multiple Choice) - Single correct answer from choices
117
+ interaction_data: {
118
+ "choices": [
119
+ { "item_body": "Answer A", "position": 1 },
120
+ { "item_body": "Answer B", "position": 2 },
121
+ { "item_body": "Answer C", "position": 3 }
122
+ ]
123
+ }
124
+ scoring_data: { "value": "<id_of_correct_choice>" }
125
+ NOTE: Create with placeholder scoring_data first. The API returns choices
126
+ with generated IDs. Then use update_quiz_item to set the correct answer ID.
127
+ Alternatively, scoring_data can reference by position index.
128
+
129
+ 2. "true-false" (True/False)
130
+ interaction_data: {
131
+ "choices": [
132
+ { "item_body": "True", "position": 1 },
133
+ { "item_body": "False", "position": 2 }
134
+ ]
135
+ }
136
+ scoring_data: { "value": "<id_of_correct_choice>" }
137
+ Same approach as "choice" - create first, then update with correct choice ID.
138
+
139
+ 3. "multi-answer" (Multiple Answer / Select All That Apply)
140
+ interaction_data: {
141
+ "choices": [
142
+ { "item_body": "Option A", "position": 1 },
143
+ { "item_body": "Option B", "position": 2 },
144
+ { "item_body": "Option C", "position": 3 }
145
+ ]
146
+ }
147
+ scoring_data: { "value": ["<id_of_correct_1>", "<id_of_correct_2>"] }
148
+
149
+ 4. "essay" (Essay / Free Response)
150
+ interaction_data: {}
151
+ scoring_data: { "value": "" }
152
+
153
+ 5. "file-upload" (File Upload)
154
+ interaction_data: {}
155
+ scoring_data: { "value": "" }
156
+
157
+ 6. "matching" (Matching)
158
+ interaction_data: {
159
+ "choices": [
160
+ { "item_body": "Term 1", "position": 1, "match_id": "m1" },
161
+ { "item_body": "Term 2", "position": 2, "match_id": "m2" }
162
+ ],
163
+ "matches": [
164
+ { "item_body": "Definition 1", "id": "m1", "position": 1 },
165
+ { "item_body": "Definition 2", "id": "m2", "position": 2 }
166
+ ]
167
+ }
168
+ scoring_data: { "value": [{ "id": "<choice_id>", "match_id": "m1" }] }
169
+
170
+ 7. "ordering" (Ordering)
171
+ interaction_data: {
172
+ "choices": [
173
+ { "item_body": "First item", "position": 1 },
174
+ { "item_body": "Second item", "position": 2 }
175
+ ]
176
+ }
177
+ scoring_data: { "value": ["<id_in_correct_order>", "<id_in_correct_order>"] }
178
+
179
+ 8. "categorization" (Categorization)
180
+ interaction_data: {
181
+ "categories": [
182
+ { "item_body": "Category A", "id": "cat1" },
183
+ { "item_body": "Category B", "id": "cat2" }
184
+ ],
185
+ "choices": [
186
+ { "item_body": "Item 1", "position": 1 },
187
+ { "item_body": "Item 2", "position": 2 }
188
+ ]
189
+ }
190
+ scoring_data: { "value": [{ "id": "<choice_id>", "category_id": "cat1" }] }
191
+
192
+ 9. "numeric" (Numeric Answer)
193
+ interaction_data: {}
194
+ scoring_data: { "value": "42" } or { "value": { "exact": 42, "margin": 0.1 } }
195
+
196
+ 10. "fill-blank" (Fill in the Blank)
197
+ interaction_data: {}
198
+ scoring_data: { "value": ["acceptable answer 1", "acceptable answer 2"] }
199
+
200
+ 11. "rich-fill-blank" (Fill in Multiple Blanks)
201
+ interaction_data: { "blanks": [{ "id": "b1", "item_body": "blank1" }] }
202
+ scoring_data: { "value": { "b1": ["answer1", "answer2"] } }
203
+
204
+ 12. "formula" (Formula / Calculated)
205
+ interaction_data: { "formula": "x + y", "variables": [{ "name": "x", "min": 1, "max": 10 }] }
206
+ scoring_data: { "value": { "formula": "x + y", "margin": 0.01 } }
207
+
208
+ 13. "hot-spot" (Hot Spot / Image Click)
209
+ interaction_data: { "image_url": "...", "regions": [...] }
210
+ scoring_data: { "value": "<region_id>" }
211
+
212
+ TIP: For choice-based questions, it is often easiest to create the item first with
213
+ an empty or placeholder scoring_data, then GET the item to see the generated choice
214
+ IDs, and finally UPDATE the item with the correct scoring_data referencing those IDs.`, {
215
+ course_id: z.string().describe("Canvas course ID"),
216
+ assignment_id: z.string().describe("The assignment ID of the New Quiz"),
217
+ position: z
218
+ .number()
219
+ .optional()
220
+ .describe("Position/order of this question in the quiz (1-based)"),
221
+ points_possible: z
222
+ .number()
223
+ .describe("Point value for this question"),
224
+ title: z.string().describe("Short title for the question"),
225
+ item_body: z
226
+ .string()
227
+ .describe("The question text/prompt (HTML supported, e.g. '<p>What is 2+2?</p>')"),
228
+ interaction_type: z
229
+ .enum([
230
+ "choice",
231
+ "true-false",
232
+ "essay",
233
+ "file-upload",
234
+ "matching",
235
+ "ordering",
236
+ "categorization",
237
+ "numeric",
238
+ "formula",
239
+ "rich-fill-blank",
240
+ "multi-answer",
241
+ "hot-spot",
242
+ "fill-blank",
243
+ ])
244
+ .describe("The question type (see tool description for formats)"),
245
+ interaction_data: z
246
+ .record(z.any())
247
+ .describe("Question-type-specific data (choices, matches, etc). See tool description for format per type."),
248
+ scoring_data: z
249
+ .record(z.any())
250
+ .describe("Correct answer data. See tool description for format per type."),
251
+ feedback_neutral: z
252
+ .string()
253
+ .optional()
254
+ .describe("Feedback shown to all students after answering (HTML supported)"),
255
+ }, async ({ course_id, assignment_id, position, points_possible, title, item_body, interaction_type, interaction_data, scoring_data, feedback_neutral, }) => {
256
+ const entry = {
257
+ title,
258
+ item_body,
259
+ calculator_type: "none",
260
+ interaction_type_slug: interaction_type,
261
+ interaction_data,
262
+ scoring_data,
263
+ };
264
+ if (feedback_neutral) {
265
+ entry.feedback = { neutral: feedback_neutral };
266
+ }
267
+ const item = {
268
+ points_possible,
269
+ properties: {},
270
+ entry_type: "Item",
271
+ entry,
272
+ };
273
+ if (position !== undefined) {
274
+ item.position = position;
275
+ }
276
+ const result = await canvas(quizApiPath(`/courses/${course_id}/quizzes/${assignment_id}/items`), {
277
+ method: "POST",
278
+ body: JSON.stringify({ item }),
279
+ });
280
+ return {
281
+ content: [
282
+ {
283
+ type: "text",
284
+ text: `Created quiz item "${title}" (ID: ${result.id})\n${JSON.stringify(result, null, 2)}`,
285
+ },
286
+ ],
287
+ };
288
+ });
289
+ // ── 7. Update Quiz Item ────────────────────────────────────────────────
290
+ server.tool("update_quiz_item", "Update an existing question (item) in a New Quiz. Only include fields you want to change. Commonly used after create_quiz_item to set the correct answer IDs in scoring_data once the choice IDs are known.", {
291
+ course_id: z.string().describe("Canvas course ID"),
292
+ assignment_id: z.string().describe("The assignment ID of the New Quiz"),
293
+ item_id: z.string().describe("The quiz item ID to update"),
294
+ position: z.number().optional().describe("Position in the quiz"),
295
+ points_possible: z.number().optional().describe("Point value"),
296
+ title: z.string().optional().describe("Question title"),
297
+ item_body: z
298
+ .string()
299
+ .optional()
300
+ .describe("Question text/prompt (HTML)"),
301
+ interaction_type: z
302
+ .enum([
303
+ "choice",
304
+ "true-false",
305
+ "essay",
306
+ "file-upload",
307
+ "matching",
308
+ "ordering",
309
+ "categorization",
310
+ "numeric",
311
+ "formula",
312
+ "rich-fill-blank",
313
+ "multi-answer",
314
+ "hot-spot",
315
+ "fill-blank",
316
+ ])
317
+ .optional()
318
+ .describe("Question type"),
319
+ interaction_data: z
320
+ .record(z.any())
321
+ .optional()
322
+ .describe("Updated choices/answers structure"),
323
+ scoring_data: z
324
+ .record(z.any())
325
+ .optional()
326
+ .describe("Updated correct answer data"),
327
+ feedback_neutral: z
328
+ .string()
329
+ .optional()
330
+ .describe("Updated feedback text (HTML)"),
331
+ }, async ({ course_id, assignment_id, item_id, position, points_possible, title, item_body, interaction_type, interaction_data, scoring_data, feedback_neutral, }) => {
332
+ const entry = {};
333
+ if (title !== undefined)
334
+ entry.title = title;
335
+ if (item_body !== undefined)
336
+ entry.item_body = item_body;
337
+ if (interaction_type !== undefined)
338
+ entry.interaction_type_slug = interaction_type;
339
+ if (interaction_data !== undefined)
340
+ entry.interaction_data = interaction_data;
341
+ if (scoring_data !== undefined)
342
+ entry.scoring_data = scoring_data;
343
+ if (feedback_neutral !== undefined)
344
+ entry.feedback = { neutral: feedback_neutral };
345
+ const item = {};
346
+ if (position !== undefined)
347
+ item.position = position;
348
+ if (points_possible !== undefined)
349
+ item.points_possible = points_possible;
350
+ if (Object.keys(entry).length > 0) {
351
+ item.entry_type = "Item";
352
+ item.entry = entry;
353
+ }
354
+ const result = await canvas(quizApiPath(`/courses/${course_id}/quizzes/${assignment_id}/items/${item_id}`), {
355
+ method: "PATCH",
356
+ body: JSON.stringify({ item }),
357
+ });
358
+ return {
359
+ content: [
360
+ {
361
+ type: "text",
362
+ text: `Updated quiz item (ID: ${result.id})\n${JSON.stringify(result, null, 2)}`,
363
+ },
364
+ ],
365
+ };
366
+ });
367
+ // ── 8. Delete Quiz Item ────────────────────────────────────────────────
368
+ server.tool("delete_quiz_item", "Delete a question (item) from a New Quiz. This cannot be undone.", {
369
+ course_id: z.string().describe("Canvas course ID"),
370
+ assignment_id: z.string().describe("The assignment ID of the New Quiz"),
371
+ item_id: z.string().describe("The quiz item ID to delete"),
372
+ }, async ({ course_id, assignment_id, item_id }) => {
373
+ await canvas(quizApiPath(`/courses/${course_id}/quizzes/${assignment_id}/items/${item_id}`), { method: "DELETE" });
374
+ return {
375
+ content: [
376
+ {
377
+ type: "text",
378
+ text: `Deleted quiz item ${item_id} from quiz ${assignment_id}`,
379
+ },
380
+ ],
381
+ };
382
+ });
383
+ // ── 9. Set Quiz Accommodations ─────────────────────────────────────────
384
+ server.tool("set_quiz_accommodations", "Set testing accommodations (extra time, extra attempts) for specific students on a New Quiz. Use list_students to find student user IDs.", {
385
+ course_id: z.string().describe("Canvas course ID"),
386
+ assignment_id: z.string().describe("The assignment ID of the New Quiz"),
387
+ student_ids: z
388
+ .array(z.string())
389
+ .describe("List of Canvas user IDs to accommodate"),
390
+ extra_time_seconds: z
391
+ .number()
392
+ .optional()
393
+ .describe("Extra time in seconds (e.g. 600 for 10 extra minutes)"),
394
+ extra_attempts: z
395
+ .number()
396
+ .optional()
397
+ .describe("Number of extra attempts to grant"),
398
+ }, async ({ course_id, assignment_id, student_ids, extra_time_seconds, extra_attempts, }) => {
399
+ const accommodations = student_ids.map((student_id) => {
400
+ const acc = { student_id };
401
+ if (extra_time_seconds !== undefined)
402
+ acc.extra_time = extra_time_seconds;
403
+ if (extra_attempts !== undefined)
404
+ acc.extra_attempts = extra_attempts;
405
+ return acc;
406
+ });
407
+ const result = await canvas(quizApiPath(`/courses/${course_id}/quizzes/${assignment_id}/accommodations`), {
408
+ method: "POST",
409
+ body: JSON.stringify({ quiz_accommodations: accommodations }),
410
+ });
411
+ return {
412
+ content: [
413
+ {
414
+ type: "text",
415
+ text: `Set accommodations for ${student_ids.length} student(s) on quiz ${assignment_id}\n${JSON.stringify(result, null, 2)}`,
416
+ },
417
+ ],
418
+ };
419
+ });
420
+ // ── 10. Generate Quiz Report ───────────────────────────────────────────
421
+ server.tool("generate_quiz_report", "Generate a student analysis or item analysis report for a New Quiz. Returns a progress object — poll the returned URL to check when the report file is ready for download.", {
422
+ course_id: z.string().describe("Canvas course ID"),
423
+ assignment_id: z.string().describe("The assignment ID of the New Quiz"),
424
+ report_type: z
425
+ .enum(["student_analysis", "item_analysis"])
426
+ .describe("Type of report: 'student_analysis' for per-student results, 'item_analysis' for per-question statistics"),
427
+ format: z
428
+ .enum(["csv", "json"])
429
+ .describe("Output format for the report"),
430
+ }, async ({ course_id, assignment_id, report_type, format }) => {
431
+ const result = await canvas(quizApiPath(`/courses/${course_id}/quizzes/${assignment_id}/reports`), {
432
+ method: "POST",
433
+ body: JSON.stringify({ report_type, format }),
434
+ });
435
+ return {
436
+ content: [
437
+ {
438
+ type: "text",
439
+ text: `Report generation started.\n${JSON.stringify(result, null, 2)}`,
440
+ },
441
+ ],
442
+ };
443
+ });
444
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerPageTools(server: McpServer): void;
@@ -0,0 +1,150 @@
1
+ import { z } from "zod";
2
+ import { canvas, canvasAll } from "../canvas-client.js";
3
+ export function registerPageTools(server) {
4
+ server.tool("list_pages", "List all wiki pages in a course. Returns summary info (url, title, published, updated_at). Use get_page for full content.", {
5
+ course_id: z.string().describe("Canvas course ID"),
6
+ search_term: z.string().optional().describe("Filter by title substring"),
7
+ published: z.boolean().optional().describe("Filter by published state"),
8
+ sort: z
9
+ .enum(["title", "created_at", "updated_at"])
10
+ .optional()
11
+ .describe("Sort order"),
12
+ }, async ({ course_id, search_term, published, sort }) => {
13
+ const params = {};
14
+ if (search_term)
15
+ params.search_term = search_term;
16
+ if (published !== undefined)
17
+ params.published = String(published);
18
+ if (sort)
19
+ params.sort = sort;
20
+ const pages = await canvasAll(`/courses/${course_id}/pages`, params);
21
+ const summary = pages.map((p) => ({
22
+ url: p.url,
23
+ title: p.title,
24
+ published: p.published,
25
+ updated_at: p.updated_at,
26
+ }));
27
+ return {
28
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
29
+ };
30
+ });
31
+ server.tool("get_page", "Get full details of a wiki page, including its body HTML content.", {
32
+ course_id: z.string().describe("Canvas course ID"),
33
+ url_or_id: z
34
+ .string()
35
+ .describe("Page URL slug (e.g. 'my-page-title') or numeric page ID"),
36
+ }, async ({ course_id, url_or_id }) => {
37
+ const page = await canvas(`/courses/${course_id}/pages/${url_or_id}`);
38
+ return {
39
+ content: [{ type: "text", text: JSON.stringify(page, null, 2) }],
40
+ };
41
+ });
42
+ server.tool("create_page", "Create a new wiki page in a course.", {
43
+ course_id: z.string().describe("Canvas course ID"),
44
+ title: z.string().describe("Page title"),
45
+ body: z.string().describe("Page content (HTML)"),
46
+ published: z.boolean().default(false).describe("Publish immediately"),
47
+ front_page: z
48
+ .boolean()
49
+ .optional()
50
+ .describe("Set as the course front page"),
51
+ editing_roles: z
52
+ .string()
53
+ .optional()
54
+ .describe("Who can edit: 'teachers', 'students', 'members', or 'public'"),
55
+ }, async ({ course_id, title, body, published, front_page, editing_roles }) => {
56
+ const wiki_page = { title, body, published };
57
+ if (front_page !== undefined)
58
+ wiki_page.front_page = front_page;
59
+ if (editing_roles)
60
+ wiki_page.editing_roles = editing_roles;
61
+ const result = await canvas(`/courses/${course_id}/pages`, {
62
+ method: "POST",
63
+ body: JSON.stringify({ wiki_page }),
64
+ });
65
+ return {
66
+ content: [
67
+ {
68
+ type: "text",
69
+ text: `Created page "${result.title}" (URL: ${result.url})\n${result.html_url}`,
70
+ },
71
+ ],
72
+ };
73
+ });
74
+ server.tool("update_page", "Update an existing wiki page. Only include fields you want to change.", {
75
+ course_id: z.string().describe("Canvas course ID"),
76
+ url_or_id: z
77
+ .string()
78
+ .describe("Page URL slug or numeric page ID"),
79
+ title: z.string().optional().describe("New page title"),
80
+ body: z.string().optional().describe("New page content (HTML)"),
81
+ published: z.boolean().optional().describe("Publish or unpublish"),
82
+ front_page: z
83
+ .boolean()
84
+ .optional()
85
+ .describe("Set or unset as front page"),
86
+ }, async ({ course_id, url_or_id, title, body, published, front_page }) => {
87
+ const wiki_page = {};
88
+ if (title !== undefined)
89
+ wiki_page.title = title;
90
+ if (body !== undefined)
91
+ wiki_page.body = body;
92
+ if (published !== undefined)
93
+ wiki_page.published = published;
94
+ if (front_page !== undefined)
95
+ wiki_page.front_page = front_page;
96
+ const result = await canvas(`/courses/${course_id}/pages/${url_or_id}`, {
97
+ method: "PUT",
98
+ body: JSON.stringify({ wiki_page }),
99
+ });
100
+ return {
101
+ content: [
102
+ {
103
+ type: "text",
104
+ text: `Updated page "${result.title}" (URL: ${result.url})`,
105
+ },
106
+ ],
107
+ };
108
+ });
109
+ server.tool("delete_page", "Delete a wiki page. This cannot be undone.", {
110
+ course_id: z.string().describe("Canvas course ID"),
111
+ url_or_id: z
112
+ .string()
113
+ .describe("Page URL slug or numeric page ID"),
114
+ }, async ({ course_id, url_or_id }) => {
115
+ await canvas(`/courses/${course_id}/pages/${url_or_id}`, {
116
+ method: "DELETE",
117
+ });
118
+ return {
119
+ content: [
120
+ { type: "text", text: `Deleted page "${url_or_id}"` },
121
+ ],
122
+ };
123
+ });
124
+ server.tool("list_page_revisions", "List revision history for a wiki page. Shows who edited and when.", {
125
+ course_id: z.string().describe("Canvas course ID"),
126
+ url_or_id: z
127
+ .string()
128
+ .describe("Page URL slug or numeric page ID"),
129
+ }, async ({ course_id, url_or_id }) => {
130
+ const revisions = await canvasAll(`/courses/${course_id}/pages/${url_or_id}/revisions`);
131
+ const summary = revisions.map((r) => ({
132
+ revision_id: r.revision_id,
133
+ updated_at: r.updated_at,
134
+ edited_by: r.edited_by
135
+ ? { id: r.edited_by.id, display_name: r.edited_by.display_name }
136
+ : null,
137
+ }));
138
+ return {
139
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
140
+ };
141
+ });
142
+ server.tool("get_front_page", "Get the course front page content.", {
143
+ course_id: z.string().describe("Canvas course ID"),
144
+ }, async ({ course_id }) => {
145
+ const page = await canvas(`/courses/${course_id}/front_page`);
146
+ return {
147
+ content: [{ type: "text", text: JSON.stringify(page, null, 2) }],
148
+ };
149
+ });
150
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerQuizTools(server: McpServer): void;