@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,626 @@
1
+ import { resolveCourseId } from "../common/helpers.js";
2
+ import { z } from "zod";
3
+ export const assignmentTools = [
4
+ {
5
+ name: "canvas_get_assignments",
6
+ tool: {
7
+ name: "canvas_get_assignments",
8
+ description: "List assignments for a course (supports search, upcoming filter, and limit)",
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
+ search: {
17
+ type: "string",
18
+ description: "Filter by assignment name (contains, case-insensitive)"
19
+ },
20
+ limit: {
21
+ type: "number",
22
+ description: "Max number of items to return (default 50, max 200)"
23
+ },
24
+ upcoming_only: {
25
+ type: "boolean",
26
+ description: "If true, only assignments with due_at >= now"
27
+ },
28
+ full: {
29
+ type: "boolean",
30
+ description: "If true, return full assignment objects. Default false returns compact payload."
31
+ }
32
+ },
33
+ required: ["course_id"],
34
+ },
35
+ },
36
+ handler: async (client, args) => {
37
+ const input = z.object({
38
+ course_id: z.union([z.number(), z.string()]),
39
+ search: z.string().optional(),
40
+ limit: z.coerce.number().optional(),
41
+ upcoming_only: z.boolean().optional(),
42
+ full: z.boolean().optional()
43
+ }).parse(args);
44
+ const courseId = await resolveCourseId(client, input.course_id);
45
+ const assignments = await client.getAssignments(courseId);
46
+ const search = (input.search || "").toLowerCase().trim();
47
+ const now = new Date();
48
+ const limit = Math.min(Math.max(input.limit ?? 50, 1), 200);
49
+ const filtered = assignments.filter((a) => {
50
+ const matchesSearch = !search || (a.name || "").toLowerCase().includes(search);
51
+ if (!matchesSearch)
52
+ return false;
53
+ if (!input.upcoming_only)
54
+ return true;
55
+ if (!a.due_at)
56
+ return false;
57
+ return new Date(a.due_at) >= now;
58
+ });
59
+ if (input.full) {
60
+ return {
61
+ content: [{ type: "text", text: JSON.stringify(filtered.slice(0, limit), null, 2) }],
62
+ };
63
+ }
64
+ const compact = filtered.slice(0, limit).map((a) => ({
65
+ id: a.id ?? null,
66
+ name: a.name,
67
+ due_at: a.due_at ?? null,
68
+ unlock_at: a.unlock_at ?? null,
69
+ lock_at: a.lock_at ?? null,
70
+ points_possible: a.points_possible ?? null,
71
+ published: a.published ?? null
72
+ }));
73
+ return {
74
+ content: [{ type: "text", text: JSON.stringify(compact, null, 2) }],
75
+ };
76
+ }
77
+ },
78
+ {
79
+ name: "canvas_get_assignment",
80
+ tool: {
81
+ name: "canvas_get_assignment",
82
+ description: "Get details for a specific assignment",
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {
86
+ course_id: {
87
+ anyOf: [{ type: "number" }, { type: "string" }],
88
+ description: "The ID or name of the course"
89
+ },
90
+ assignment_id: { type: "number", description: "The ID of the assignment" },
91
+ },
92
+ required: ["course_id", "assignment_id"],
93
+ },
94
+ },
95
+ handler: async (client, args) => {
96
+ const input = z.object({
97
+ course_id: z.union([z.number(), z.string()]),
98
+ assignment_id: z.coerce.number()
99
+ }).parse(args);
100
+ const courseId = await resolveCourseId(client, input.course_id);
101
+ const assignment = await client.getAssignment(courseId, input.assignment_id);
102
+ return {
103
+ content: [{ type: "text", text: JSON.stringify(assignment, null, 2) }],
104
+ };
105
+ }
106
+ },
107
+ {
108
+ name: "canvas_list_assignment_groups",
109
+ tool: {
110
+ name: "canvas_list_assignment_groups",
111
+ description: "List all assignment groups in a course",
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: {
115
+ course_id: {
116
+ anyOf: [{ type: "number" }, { type: "string" }],
117
+ description: "The ID or name of the course"
118
+ },
119
+ },
120
+ required: ["course_id"],
121
+ },
122
+ },
123
+ handler: async (client, args) => {
124
+ const input = z.object({ course_id: z.union([z.number(), z.string()]) }).parse(args);
125
+ const courseId = await resolveCourseId(client, input.course_id);
126
+ const groups = await client.getAssignmentGroups(courseId);
127
+ return {
128
+ content: [{ type: "text", text: JSON.stringify(groups, null, 2) }],
129
+ };
130
+ }
131
+ },
132
+ {
133
+ name: "canvas_get_submissions",
134
+ tool: {
135
+ name: "canvas_get_submissions",
136
+ description: "Get submissions for a specific assignment in a course",
137
+ inputSchema: {
138
+ type: "object",
139
+ properties: {
140
+ course_id: {
141
+ anyOf: [{ type: "number" }, { type: "string" }],
142
+ description: "The ID or name of the course"
143
+ },
144
+ assignment_id: { type: "number", description: "The ID of the assignment" },
145
+ },
146
+ required: ["course_id", "assignment_id"],
147
+ },
148
+ },
149
+ handler: async (client, args) => {
150
+ const input = z.object({
151
+ course_id: z.union([z.number(), z.string()]),
152
+ assignment_id: z.coerce.number()
153
+ }).parse(args);
154
+ const courseId = await resolveCourseId(client, input.course_id);
155
+ const submissions = await client.getSubmissions(courseId, input.assignment_id);
156
+ return {
157
+ content: [{ type: "text", text: JSON.stringify(submissions, null, 2) }],
158
+ };
159
+ }
160
+ },
161
+ {
162
+ name: "canvas_get_submission",
163
+ tool: {
164
+ name: "canvas_get_submission",
165
+ description: "Get a specific submission details, including file download URLs and text content",
166
+ inputSchema: {
167
+ type: "object",
168
+ properties: {
169
+ course_id: {
170
+ anyOf: [{ type: "number" }, { type: "string" }],
171
+ description: "The ID or name of the course"
172
+ },
173
+ assignment_id: { type: "number", description: "The ID of the assignment" },
174
+ student_id: { type: "number", description: "The ID of the student" },
175
+ },
176
+ required: ["course_id", "assignment_id", "student_id"],
177
+ },
178
+ },
179
+ handler: async (client, args) => {
180
+ const input = z.object({
181
+ course_id: z.union([z.number(), z.string()]),
182
+ assignment_id: z.coerce.number(),
183
+ student_id: z.coerce.number()
184
+ }).parse(args);
185
+ const courseId = await resolveCourseId(client, input.course_id);
186
+ const submission = await client.getSingleSubmission(courseId, input.assignment_id, input.student_id);
187
+ return {
188
+ content: [{ type: "text", text: JSON.stringify(submission, null, 2) }],
189
+ };
190
+ }
191
+ },
192
+ {
193
+ name: "canvas_get_submission_comments",
194
+ tool: {
195
+ name: "canvas_get_submission_comments",
196
+ description: "Get all comments for a specific submission",
197
+ inputSchema: {
198
+ type: "object",
199
+ properties: {
200
+ course_id: {
201
+ anyOf: [{ type: "number" }, { type: "string" }],
202
+ description: "The ID or name of the course"
203
+ },
204
+ assignment_id: { type: "number", description: "The ID of the assignment" },
205
+ student_id: { type: "number", description: "The ID of the student" },
206
+ },
207
+ required: ["course_id", "assignment_id", "student_id"],
208
+ },
209
+ },
210
+ handler: async (client, args) => {
211
+ const input = z.object({
212
+ course_id: z.union([z.number(), z.string()]),
213
+ assignment_id: z.coerce.number(),
214
+ student_id: z.coerce.number()
215
+ }).parse(args);
216
+ const courseId = await resolveCourseId(client, input.course_id);
217
+ const submission = await client.getSubmissionComments(courseId, input.assignment_id, input.student_id);
218
+ return {
219
+ content: [{ type: "text", text: JSON.stringify(submission.submission_comments || [], null, 2) }],
220
+ };
221
+ }
222
+ },
223
+ {
224
+ name: "canvas_delete_submission_comment",
225
+ tool: {
226
+ name: "canvas_delete_submission_comment",
227
+ description: "Delete a specific submission comment",
228
+ inputSchema: {
229
+ type: "object",
230
+ properties: {
231
+ course_id: {
232
+ anyOf: [{ type: "number" }, { type: "string" }],
233
+ description: "The ID or name of the course"
234
+ },
235
+ assignment_id: { type: "number", description: "The ID of the assignment" },
236
+ student_id: { type: "number", description: "The ID of the student" },
237
+ comment_id: { type: "number", description: "The ID of the comment to delete" },
238
+ },
239
+ required: ["course_id", "assignment_id", "student_id", "comment_id"],
240
+ },
241
+ },
242
+ handler: async (client, args) => {
243
+ const input = z.object({
244
+ course_id: z.union([z.number(), z.string()]),
245
+ assignment_id: z.coerce.number(),
246
+ student_id: z.coerce.number(),
247
+ comment_id: z.coerce.number()
248
+ }).parse(args);
249
+ const courseId = await resolveCourseId(client, input.course_id);
250
+ const result = await client.deleteSubmissionComment(courseId, input.assignment_id, input.student_id, input.comment_id);
251
+ return {
252
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
253
+ };
254
+ }
255
+ },
256
+ {
257
+ name: "canvas_update_assignment_dates",
258
+ tool: {
259
+ name: "canvas_update_assignment_dates",
260
+ description: "Update due/unlock/lock dates for a specific assignment",
261
+ inputSchema: {
262
+ type: "object",
263
+ properties: {
264
+ course_id: {
265
+ anyOf: [{ type: "number" }, { type: "string" }],
266
+ description: "The ID or name of the course"
267
+ },
268
+ assignment_id: { type: "number", description: "The ID of the assignment" },
269
+ due_at: {
270
+ anyOf: [{ type: "string" }, { type: "null" }],
271
+ description: "New due date in ISO-8601 format (e.g., 2026-02-15T23:59:00Z). Use null to clear."
272
+ },
273
+ unlock_at: {
274
+ anyOf: [{ type: "string" }, { type: "null" }],
275
+ description: "New unlock date in ISO-8601 format. Use null to clear."
276
+ },
277
+ lock_at: {
278
+ anyOf: [{ type: "string" }, { type: "null" }],
279
+ description: "New lock date in ISO-8601 format. Use null to clear."
280
+ }
281
+ },
282
+ required: ["course_id", "assignment_id"]
283
+ },
284
+ },
285
+ handler: async (client, args) => {
286
+ const input = z.object({
287
+ course_id: z.union([z.number(), z.string()]),
288
+ assignment_id: z.coerce.number(),
289
+ due_at: z.string().nullable().optional(),
290
+ unlock_at: z.string().nullable().optional(),
291
+ lock_at: z.string().nullable().optional()
292
+ }).parse(args);
293
+ if (input.due_at === undefined && input.unlock_at === undefined && input.lock_at === undefined) {
294
+ throw new Error("At least one date field is required: due_at, unlock_at, or lock_at.");
295
+ }
296
+ const courseId = await resolveCourseId(client, input.course_id);
297
+ const updatedAssignment = await client.updateAssignmentDates(courseId, input.assignment_id, {
298
+ due_at: input.due_at,
299
+ unlock_at: input.unlock_at,
300
+ lock_at: input.lock_at
301
+ });
302
+ return {
303
+ content: [{ type: "text", text: JSON.stringify(updatedAssignment, null, 2) }],
304
+ };
305
+ }
306
+ },
307
+ {
308
+ name: "canvas_bulk_update_assignment_due_date_by_query",
309
+ tool: {
310
+ name: "canvas_bulk_update_assignment_due_date_by_query",
311
+ description: "Update due date in bulk for assignments matched by query terms in assignment name",
312
+ inputSchema: {
313
+ type: "object",
314
+ properties: {
315
+ course_id: {
316
+ anyOf: [{ type: "number" }, { type: "string" }],
317
+ description: "The ID or name of the course"
318
+ },
319
+ query_terms: {
320
+ type: "array",
321
+ items: { type: "string" },
322
+ description: "All terms must appear in assignment name (case-insensitive)"
323
+ },
324
+ due_at: {
325
+ type: "string",
326
+ description: "ISO-8601 due date to apply (e.g., 2026-02-12T23:59:00-05:00)"
327
+ },
328
+ limit: {
329
+ type: "number",
330
+ description: "Max assignments to process (default 20, max 100)"
331
+ },
332
+ dry_run: {
333
+ type: "boolean",
334
+ description: "If true, show matches without applying updates"
335
+ }
336
+ },
337
+ required: ["course_id", "query_terms", "due_at"]
338
+ }
339
+ },
340
+ handler: async (client, args) => {
341
+ const input = z.object({
342
+ course_id: z.union([z.number(), z.string()]),
343
+ query_terms: z.array(z.string()).min(1),
344
+ due_at: z.string(),
345
+ limit: z.coerce.number().optional(),
346
+ dry_run: z.boolean().optional()
347
+ }).parse(args);
348
+ const courseId = await resolveCourseId(client, input.course_id);
349
+ const terms = input.query_terms.map((t) => t.toLowerCase().trim()).filter(Boolean);
350
+ const limit = Math.min(Math.max(input.limit ?? 20, 1), 100);
351
+ const dryRun = input.dry_run === true;
352
+ const assignments = await client.getAssignments(courseId);
353
+ const matched = assignments
354
+ .filter((a) => {
355
+ const name = (a.name || "").toLowerCase();
356
+ return terms.every((term) => name.includes(term));
357
+ })
358
+ .slice(0, limit);
359
+ const results = [];
360
+ for (const assignment of matched) {
361
+ if (!assignment.id)
362
+ continue;
363
+ if (dryRun) {
364
+ results.push({
365
+ assignment_id: assignment.id,
366
+ assignment_name: assignment.name,
367
+ old_due_at: assignment.due_at ?? null,
368
+ new_due_at: input.due_at,
369
+ status: "matched_only"
370
+ });
371
+ continue;
372
+ }
373
+ try {
374
+ const updated = await client.updateAssignmentDates(courseId, assignment.id, {
375
+ due_at: input.due_at
376
+ });
377
+ results.push({
378
+ assignment_id: updated.id ?? assignment.id,
379
+ assignment_name: updated.name,
380
+ old_due_at: assignment.due_at ?? null,
381
+ new_due_at: updated.due_at ?? input.due_at,
382
+ status: "updated"
383
+ });
384
+ }
385
+ catch (error) {
386
+ results.push({
387
+ assignment_id: assignment.id,
388
+ assignment_name: assignment.name,
389
+ old_due_at: assignment.due_at ?? null,
390
+ new_due_at: input.due_at,
391
+ status: "error",
392
+ error: error?.message || "Unknown error"
393
+ });
394
+ }
395
+ }
396
+ return {
397
+ content: [{
398
+ type: "text",
399
+ text: JSON.stringify({
400
+ course_id: courseId,
401
+ matched_count: matched.length,
402
+ updated_count: results.filter((r) => r.status === "updated").length,
403
+ dry_run: dryRun,
404
+ results
405
+ }, null, 2)
406
+ }],
407
+ };
408
+ }
409
+ },
410
+ {
411
+ name: "canvas_create_assignment",
412
+ tool: {
413
+ name: "canvas_create_assignment",
414
+ description: "Create a new assignment for a course",
415
+ inputSchema: {
416
+ type: "object",
417
+ properties: {
418
+ course_id: {
419
+ anyOf: [{ type: "number" }, { type: "string" }],
420
+ description: "The ID or name of the course"
421
+ },
422
+ name: {
423
+ type: "string",
424
+ description: "The name of the assignment (required)"
425
+ },
426
+ description: {
427
+ type: "string",
428
+ description: "The assignment's description, supports HTML"
429
+ },
430
+ submission_types: {
431
+ type: "array",
432
+ items: {
433
+ type: "string",
434
+ enum: ["online_upload", "online_text_entry", "online_url", "media_recording", "student_annotation", "online_quiz", "none", "on_paper", "discussion_topic", "external_tool"]
435
+ },
436
+ description: "List of supported submission types"
437
+ },
438
+ points_possible: {
439
+ type: "number",
440
+ description: "The maximum points possible on the assignment"
441
+ },
442
+ grading_type: {
443
+ type: "string",
444
+ enum: ["pass_fail", "percent", "letter_grade", "gpa_scale", "points", "not_graded"],
445
+ description: "The strategy used for grading the assignment"
446
+ },
447
+ due_at: {
448
+ type: "string",
449
+ description: "The day/time the assignment is due (ISO 8601)"
450
+ },
451
+ lock_at: {
452
+ type: "string",
453
+ description: "The day/time the assignment is locked after (ISO 8601)"
454
+ },
455
+ unlock_at: {
456
+ type: "string",
457
+ description: "The day/time the assignment is unlocked (ISO 8601)"
458
+ },
459
+ assignment_group_id: {
460
+ type: "number",
461
+ description: "The assignment group id to put the assignment in"
462
+ },
463
+ published: {
464
+ type: "boolean",
465
+ description: "Whether this assignment is published"
466
+ },
467
+ allowed_extensions: {
468
+ type: "array",
469
+ items: { type: "string" },
470
+ description: "Allowed extensions if submission_types includes 'online_upload'"
471
+ }
472
+ },
473
+ required: ["course_id", "name"]
474
+ },
475
+ },
476
+ handler: async (client, args) => {
477
+ const input = z.object({
478
+ course_id: z.union([z.number(), z.string()]),
479
+ name: z.string(),
480
+ description: z.string().optional(),
481
+ submission_types: z.array(z.string()).optional(),
482
+ points_possible: z.number().optional(),
483
+ grading_type: z.string().optional(),
484
+ due_at: z.string().optional(),
485
+ lock_at: z.string().optional(),
486
+ unlock_at: z.string().optional(),
487
+ assignment_group_id: z.number().optional(),
488
+ published: z.boolean().optional(),
489
+ allowed_extensions: z.array(z.string()).optional()
490
+ }).parse(args);
491
+ const courseId = await resolveCourseId(client, input.course_id);
492
+ const assignmentData = {
493
+ name: input.name
494
+ };
495
+ if (input.description !== undefined)
496
+ assignmentData.description = input.description;
497
+ if (input.submission_types !== undefined)
498
+ assignmentData.submission_types = input.submission_types;
499
+ if (input.points_possible !== undefined)
500
+ assignmentData.points_possible = input.points_possible;
501
+ if (input.grading_type !== undefined)
502
+ assignmentData.grading_type = input.grading_type;
503
+ if (input.due_at !== undefined)
504
+ assignmentData.due_at = input.due_at;
505
+ if (input.lock_at !== undefined)
506
+ assignmentData.lock_at = input.lock_at;
507
+ if (input.unlock_at !== undefined)
508
+ assignmentData.unlock_at = input.unlock_at;
509
+ if (input.assignment_group_id !== undefined)
510
+ assignmentData.assignment_group_id = input.assignment_group_id;
511
+ if (input.published !== undefined)
512
+ assignmentData.published = input.published;
513
+ if (input.allowed_extensions !== undefined)
514
+ assignmentData.allowed_extensions = input.allowed_extensions;
515
+ const assignment = await client.createAssignment(courseId, assignmentData);
516
+ return {
517
+ content: [{ type: "text", text: JSON.stringify(assignment, null, 2) }],
518
+ };
519
+ }
520
+ },
521
+ {
522
+ name: "canvas_update_assignment",
523
+ tool: {
524
+ name: "canvas_update_assignment",
525
+ description: "Update an existing assignment",
526
+ inputSchema: {
527
+ type: "object",
528
+ properties: {
529
+ course_id: {
530
+ anyOf: [{ type: "number" }, { type: "string" }],
531
+ description: "The ID or name of the course"
532
+ },
533
+ assignment_id: {
534
+ type: "number",
535
+ description: "The ID of the assignment to update"
536
+ },
537
+ name: {
538
+ type: "string",
539
+ description: "The name of the assignment"
540
+ },
541
+ description: {
542
+ type: "string",
543
+ description: "The assignment's description, supports HTML"
544
+ },
545
+ submission_types: {
546
+ type: "array",
547
+ items: {
548
+ type: "string",
549
+ enum: ["online_upload", "online_text_entry", "online_url", "media_recording", "student_annotation", "online_quiz", "none", "on_paper", "discussion_topic", "external_tool"]
550
+ },
551
+ description: "List of supported submission types"
552
+ },
553
+ points_possible: {
554
+ type: "number",
555
+ description: "The maximum points possible on the assignment"
556
+ },
557
+ grading_type: {
558
+ type: "string",
559
+ enum: ["pass_fail", "percent", "letter_grade", "gpa_scale", "points", "not_graded"],
560
+ description: "The strategy used for grading the assignment"
561
+ },
562
+ due_at: {
563
+ type: "string",
564
+ description: "The day/time the assignment is due (ISO 8601)"
565
+ },
566
+ lock_at: {
567
+ type: "string",
568
+ description: "The day/time the assignment is locked after (ISO 8601)"
569
+ },
570
+ unlock_at: {
571
+ type: "string",
572
+ description: "The day/time the assignment is unlocked (ISO 8601)"
573
+ },
574
+ assignment_group_id: {
575
+ type: "number",
576
+ description: "The assignment group id to put the assignment in"
577
+ },
578
+ published: {
579
+ type: "boolean",
580
+ description: "Whether this assignment is published"
581
+ },
582
+ allowed_extensions: {
583
+ type: "array",
584
+ items: { type: "string" },
585
+ description: "Allowed extensions if submission_types includes 'online_upload'"
586
+ }
587
+ },
588
+ required: ["course_id", "assignment_id"]
589
+ },
590
+ },
591
+ handler: async (client, args) => {
592
+ const input = z.object({
593
+ course_id: z.union([z.number(), z.string()]),
594
+ assignment_id: z.coerce.number(),
595
+ name: z.string().optional(),
596
+ description: z.string().optional(),
597
+ submission_types: z.array(z.string()).optional(),
598
+ points_possible: z.number().optional(),
599
+ grading_type: z.string().optional(),
600
+ due_at: z.string().optional(),
601
+ lock_at: z.string().optional(),
602
+ unlock_at: z.string().optional(),
603
+ assignment_group_id: z.number().optional(),
604
+ published: z.boolean().optional(),
605
+ allowed_extensions: z.array(z.string()).optional()
606
+ }).parse(args);
607
+ const courseId = await resolveCourseId(client, input.course_id);
608
+ const assignment = await client.updateAssignment(courseId, input.assignment_id, {
609
+ name: input.name,
610
+ description: input.description,
611
+ submission_types: input.submission_types,
612
+ points_possible: input.points_possible,
613
+ grading_type: input.grading_type,
614
+ due_at: input.due_at,
615
+ lock_at: input.lock_at,
616
+ unlock_at: input.unlock_at,
617
+ assignment_group_id: input.assignment_group_id,
618
+ published: input.published,
619
+ allowed_extensions: input.allowed_extensions
620
+ });
621
+ return {
622
+ content: [{ type: "text", text: JSON.stringify(assignment, null, 2) }],
623
+ };
624
+ }
625
+ }
626
+ ];