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.
- package/README.md +41 -0
- package/dist/canvas-client.d.ts +24 -0
- package/dist/canvas-client.js +90 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +41 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +287 -0
- package/dist/tools/analytics.d.ts +2 -0
- package/dist/tools/analytics.js +69 -0
- package/dist/tools/assignments.d.ts +2 -0
- package/dist/tools/assignments.js +175 -0
- package/dist/tools/calendar.d.ts +2 -0
- package/dist/tools/calendar.js +119 -0
- package/dist/tools/courses.d.ts +2 -0
- package/dist/tools/courses.js +52 -0
- package/dist/tools/discussions.d.ts +2 -0
- package/dist/tools/discussions.js +134 -0
- package/dist/tools/enrollments.d.ts +2 -0
- package/dist/tools/enrollments.js +105 -0
- package/dist/tools/files.d.ts +2 -0
- package/dist/tools/files.js +148 -0
- package/dist/tools/grading.d.ts +2 -0
- package/dist/tools/grading.js +260 -0
- package/dist/tools/modules.d.ts +2 -0
- package/dist/tools/modules.js +215 -0
- package/dist/tools/new-quizzes.d.ts +2 -0
- package/dist/tools/new-quizzes.js +444 -0
- package/dist/tools/pages.d.ts +2 -0
- package/dist/tools/pages.js +150 -0
- package/dist/tools/quizzes.d.ts +2 -0
- package/dist/tools/quizzes.js +83 -0
- package/dist/tools/rubrics.d.ts +2 -0
- package/dist/tools/rubrics.js +298 -0
- package/dist/tools/scheduling.d.ts +2 -0
- package/dist/tools/scheduling.js +133 -0
- package/dist/tools/submissions.d.ts +2 -0
- package/dist/tools/submissions.js +150 -0
- package/package.json +43 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { canvas, canvasAll, summarizeItem } from "../canvas-client.js";
|
|
3
|
+
export function registerAssignmentTools(server) {
|
|
4
|
+
server.tool("list_assignments", "List assignments in a course. This includes regular assignments, graded discussions, and New Quizzes (which are all assignments internally). Returns summary info (id, name, dates, points). Use get_assignment for full details.", {
|
|
5
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
6
|
+
search_term: z.string().optional().describe("Filter by name substring"),
|
|
7
|
+
assignment_group_id: z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("Filter to a specific assignment group"),
|
|
11
|
+
order_by: z
|
|
12
|
+
.enum(["due_at", "name", "position"])
|
|
13
|
+
.default("position")
|
|
14
|
+
.describe("Sort order"),
|
|
15
|
+
}, async ({ course_id, search_term, assignment_group_id, order_by }) => {
|
|
16
|
+
const params = { order_by };
|
|
17
|
+
if (search_term)
|
|
18
|
+
params.search_term = search_term;
|
|
19
|
+
if (assignment_group_id)
|
|
20
|
+
params.assignment_group_id = assignment_group_id;
|
|
21
|
+
const assignments = await canvasAll(`/courses/${course_id}/assignments`, params);
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: JSON.stringify(assignments.map(summarizeItem), null, 2),
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
server.tool("get_assignment", "Get full details of a single assignment, including its description/instructions.", {
|
|
32
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
33
|
+
assignment_id: z.string().describe("Assignment ID"),
|
|
34
|
+
}, async ({ course_id, assignment_id }) => {
|
|
35
|
+
const assignment = await canvas(`/courses/${course_id}/assignments/${assignment_id}`);
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{ type: "text", text: JSON.stringify(assignment, null, 2) },
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
server.tool("create_assignment", "Create a new regular assignment in a course. For graded discussions, use create_discussion with points_possible instead. For New Quizzes, use create_new_quiz instead.", {
|
|
43
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
44
|
+
name: z.string().describe("Assignment name"),
|
|
45
|
+
description: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Instructions/description (HTML supported)"),
|
|
49
|
+
due_at: z.string().optional().describe("Due date (ISO 8601)"),
|
|
50
|
+
unlock_at: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Available from date (ISO 8601)"),
|
|
54
|
+
lock_at: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Available until date (ISO 8601)"),
|
|
58
|
+
points_possible: z.number().optional().describe("Point value"),
|
|
59
|
+
submission_types: z
|
|
60
|
+
.array(z.string())
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("e.g. ['online_text_entry','online_upload','online_url','discussion_topic','external_tool']"),
|
|
63
|
+
assignment_group_id: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Assignment group ID to place it in"),
|
|
67
|
+
published: z.boolean().default(false).describe("Publish immediately"),
|
|
68
|
+
position: z.number().optional().describe("Position within the group"),
|
|
69
|
+
}, async ({ course_id, ...params }) => {
|
|
70
|
+
const result = await canvas(`/courses/${course_id}/assignments`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: JSON.stringify({ assignment: params }),
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `Created assignment "${result.name}" (ID: ${result.id})\n${result.html_url}`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
server.tool("update_assignment", "Update an existing assignment. Only include fields you want to change. Also works for graded discussions and New Quizzes (via their assignment_id). For date-only changes, prefer update_assignment_dates which is more focused.", {
|
|
84
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
85
|
+
assignment_id: z.string().describe("Assignment ID"),
|
|
86
|
+
name: z.string().optional(),
|
|
87
|
+
description: z.string().optional().describe("HTML instructions"),
|
|
88
|
+
due_at: z.string().optional().describe("ISO 8601 or empty string to clear"),
|
|
89
|
+
unlock_at: z.string().optional().describe("Available from (ISO 8601)"),
|
|
90
|
+
lock_at: z.string().optional().describe("Available until (ISO 8601)"),
|
|
91
|
+
points_possible: z.number().optional(),
|
|
92
|
+
submission_types: z.array(z.string()).optional().describe("e.g. ['online_text_entry','online_upload','online_url']"),
|
|
93
|
+
assignment_group_id: z.string().optional().describe("Group ID (use list_assignment_groups to find)"),
|
|
94
|
+
published: z.boolean().optional(),
|
|
95
|
+
position: z.number().optional(),
|
|
96
|
+
}, async ({ course_id, assignment_id, ...params }) => {
|
|
97
|
+
const result = await canvas(`/courses/${course_id}/assignments/${assignment_id}`, {
|
|
98
|
+
method: "PUT",
|
|
99
|
+
body: JSON.stringify({ assignment: params }),
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: `Updated "${result.name}" (ID: ${result.id})`,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
server.tool("delete_assignment", "Permanently delete an assignment. This cannot be undone.", {
|
|
111
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
112
|
+
assignment_id: z.string().describe("Assignment ID"),
|
|
113
|
+
confirm_name: z
|
|
114
|
+
.string()
|
|
115
|
+
.describe("Type the assignment name to confirm deletion (safety check)"),
|
|
116
|
+
}, async ({ course_id, assignment_id, confirm_name }) => {
|
|
117
|
+
// Verify the name matches before deleting
|
|
118
|
+
const assignment = await canvas(`/courses/${course_id}/assignments/${assignment_id}`);
|
|
119
|
+
if (assignment.name !== confirm_name) {
|
|
120
|
+
return {
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: "text",
|
|
124
|
+
text: `Safety check failed: assignment name is "${assignment.name}" but you confirmed "${confirm_name}". Delete aborted.`,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
await canvas(`/courses/${course_id}/assignments/${assignment_id}`, {
|
|
130
|
+
method: "DELETE",
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{ type: "text", text: `Deleted assignment "${confirm_name}"` },
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
server.tool("batch_update_assignments", "Update fields across multiple assignments at once. Does NOT support date fields — use batch_update_dates for date changes. Applies the same update to each assignment ID provided.", {
|
|
139
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
140
|
+
assignment_ids: z
|
|
141
|
+
.array(z.string())
|
|
142
|
+
.describe("List of assignment IDs to update"),
|
|
143
|
+
updates: z
|
|
144
|
+
.object({
|
|
145
|
+
description: z.string().optional(),
|
|
146
|
+
points_possible: z.number().optional(),
|
|
147
|
+
submission_types: z.array(z.string()).optional(),
|
|
148
|
+
published: z.boolean().optional(),
|
|
149
|
+
assignment_group_id: z.string().optional(),
|
|
150
|
+
})
|
|
151
|
+
.describe("Fields to update on each assignment"),
|
|
152
|
+
}, async ({ course_id, assignment_ids, updates }) => {
|
|
153
|
+
const results = [];
|
|
154
|
+
for (const id of assignment_ids) {
|
|
155
|
+
try {
|
|
156
|
+
const result = await canvas(`/courses/${course_id}/assignments/${id}`, {
|
|
157
|
+
method: "PUT",
|
|
158
|
+
body: JSON.stringify({ assignment: updates }),
|
|
159
|
+
});
|
|
160
|
+
results.push(` OK: "${result.name}" (${id})`);
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
results.push(` FAILED: ${id} — ${e.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: "text",
|
|
170
|
+
text: `Batch update complete (${assignment_ids.length} assignments):\n${results.join("\n")}`,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { canvas, canvasAll } from "../canvas-client.js";
|
|
3
|
+
export function registerCalendarTools(server) {
|
|
4
|
+
server.tool("list_calendar_events", "List calendar events, optionally filtered by course, date range, or type.", {
|
|
5
|
+
course_id: z
|
|
6
|
+
.string()
|
|
7
|
+
.optional()
|
|
8
|
+
.describe("Filter to a specific course ID"),
|
|
9
|
+
start_date: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Start of date range (ISO 8601)"),
|
|
13
|
+
end_date: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("End of date range (ISO 8601)"),
|
|
17
|
+
type: z
|
|
18
|
+
.enum(["event", "assignment"])
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Filter by event type"),
|
|
21
|
+
}, async ({ course_id, start_date, end_date, type }) => {
|
|
22
|
+
const params = {};
|
|
23
|
+
if (course_id)
|
|
24
|
+
params["context_codes[]"] = `course_${course_id}`;
|
|
25
|
+
if (start_date)
|
|
26
|
+
params.start_date = start_date;
|
|
27
|
+
if (end_date)
|
|
28
|
+
params.end_date = end_date;
|
|
29
|
+
if (type)
|
|
30
|
+
params.type = type;
|
|
31
|
+
const events = await canvasAll("/calendar_events", params);
|
|
32
|
+
const summary = events.map((e) => ({
|
|
33
|
+
id: e.id,
|
|
34
|
+
title: e.title,
|
|
35
|
+
start_at: e.start_at,
|
|
36
|
+
end_at: e.end_at,
|
|
37
|
+
description: e.description
|
|
38
|
+
? e.description.substring(0, 200) +
|
|
39
|
+
(e.description.length > 200 ? "..." : "")
|
|
40
|
+
: null,
|
|
41
|
+
location_name: e.location_name,
|
|
42
|
+
type: e.type,
|
|
43
|
+
}));
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
server.tool("create_calendar_event", "Create a standalone calendar event (e.g. office hours, class sessions, review sessions). These are NOT assignments — to create graded items with due dates, use create_assignment.", {
|
|
49
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
50
|
+
title: z.string().describe("Event title"),
|
|
51
|
+
start_at: z.string().describe("Start date/time (ISO 8601)"),
|
|
52
|
+
end_at: z.string().describe("End date/time (ISO 8601)"),
|
|
53
|
+
description: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Event description (HTML supported)"),
|
|
57
|
+
location_name: z.string().optional().describe("Location name"),
|
|
58
|
+
location_address: z.string().optional().describe("Location address"),
|
|
59
|
+
}, async ({ course_id, title, start_at, end_at, description, location_name, location_address }) => {
|
|
60
|
+
const result = await canvas("/calendar_events", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
calendar_event: {
|
|
64
|
+
context_code: `course_${course_id}`,
|
|
65
|
+
title,
|
|
66
|
+
start_at,
|
|
67
|
+
end_at,
|
|
68
|
+
description,
|
|
69
|
+
location_name,
|
|
70
|
+
location_address,
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `Created calendar event "${result.title}" (ID: ${result.id})`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
server.tool("update_calendar_event", "Update an existing calendar event. Only include fields you want to change.", {
|
|
84
|
+
event_id: z.string().describe("Calendar event ID"),
|
|
85
|
+
title: z.string().optional(),
|
|
86
|
+
start_at: z.string().optional().describe("ISO 8601"),
|
|
87
|
+
end_at: z.string().optional().describe("ISO 8601"),
|
|
88
|
+
description: z.string().optional().describe("HTML description"),
|
|
89
|
+
location_name: z.string().optional(),
|
|
90
|
+
}, async ({ event_id, ...params }) => {
|
|
91
|
+
const result = await canvas(`/calendar_events/${event_id}`, {
|
|
92
|
+
method: "PUT",
|
|
93
|
+
body: JSON.stringify({ calendar_event: params }),
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: `Updated calendar event "${result.title}" (ID: ${result.id})`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
server.tool("delete_calendar_event", "Delete a calendar event.", {
|
|
105
|
+
event_id: z.string().describe("Calendar event ID"),
|
|
106
|
+
}, async ({ event_id }) => {
|
|
107
|
+
await canvas(`/calendar_events/${event_id}`, {
|
|
108
|
+
method: "DELETE",
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: `Deleted calendar event ${event_id}`,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { canvasAll } from "../canvas-client.js";
|
|
3
|
+
export function registerCourseTools(server) {
|
|
4
|
+
server.tool("list_courses", "List Canvas courses you have access to. Returns course IDs needed by all other tools. Shows active courses by default.", {
|
|
5
|
+
enrollment_state: z
|
|
6
|
+
.enum(["active", "completed", "invited"])
|
|
7
|
+
.default("active")
|
|
8
|
+
.describe("Filter by enrollment state"),
|
|
9
|
+
}, async ({ enrollment_state }) => {
|
|
10
|
+
const courses = await canvasAll("/courses", {
|
|
11
|
+
enrollment_state,
|
|
12
|
+
include: "term",
|
|
13
|
+
});
|
|
14
|
+
const summary = courses.map((c) => ({
|
|
15
|
+
id: c.id,
|
|
16
|
+
name: c.name,
|
|
17
|
+
course_code: c.course_code,
|
|
18
|
+
term: c.term?.name,
|
|
19
|
+
workflow_state: c.workflow_state,
|
|
20
|
+
}));
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
server.tool("list_assignment_groups", "List assignment groups (grading categories) in a course with their weights. The returned group IDs are used as assignment_group_id when creating or updating assignments.", { course_id: z.string().describe("Canvas course ID") }, async ({ course_id }) => {
|
|
26
|
+
const groups = await canvasAll(`/courses/${course_id}/assignment_groups`);
|
|
27
|
+
const summary = groups.map((g) => ({
|
|
28
|
+
id: g.id,
|
|
29
|
+
name: g.name,
|
|
30
|
+
position: g.position,
|
|
31
|
+
group_weight: g.group_weight,
|
|
32
|
+
}));
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
server.tool("list_modules", "List modules in a course. For module management (create, update, delete, add items), see create_module, update_module, add_module_item, and related tools.", {
|
|
38
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
39
|
+
include_items: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.default(false)
|
|
42
|
+
.describe("Include module items in the response"),
|
|
43
|
+
}, async ({ course_id, include_items }) => {
|
|
44
|
+
const params = {};
|
|
45
|
+
if (include_items)
|
|
46
|
+
params.include = "items";
|
|
47
|
+
const modules = await canvasAll(`/courses/${course_id}/modules`, params);
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: JSON.stringify(modules, null, 2) }],
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { canvas, canvasAll } from "../canvas-client.js";
|
|
3
|
+
export function registerDiscussionTools(server) {
|
|
4
|
+
server.tool("list_discussions", "List discussion topics in a course. Graded discussions also appear as assignments (with assignment_id in the response). To update dates or points of graded discussions, use update_assignment_dates or update_assignment with the assignment_id.", {
|
|
5
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
6
|
+
search_term: z.string().optional().describe("Filter by title substring"),
|
|
7
|
+
only_announcements: z
|
|
8
|
+
.boolean()
|
|
9
|
+
.default(false)
|
|
10
|
+
.describe("Only return announcements"),
|
|
11
|
+
}, async ({ course_id, search_term, only_announcements }) => {
|
|
12
|
+
const params = {};
|
|
13
|
+
if (search_term)
|
|
14
|
+
params.search_term = search_term;
|
|
15
|
+
if (only_announcements)
|
|
16
|
+
params.only_announcements = "true";
|
|
17
|
+
const discussions = await canvasAll(`/courses/${course_id}/discussion_topics`, params);
|
|
18
|
+
const summary = discussions.map((d) => ({
|
|
19
|
+
id: d.id,
|
|
20
|
+
title: d.title,
|
|
21
|
+
due_at: d.assignment?.due_at ?? null,
|
|
22
|
+
unlock_at: d.assignment?.unlock_at ?? d.delayed_post_at,
|
|
23
|
+
lock_at: d.assignment?.lock_at ?? d.lock_at,
|
|
24
|
+
points_possible: d.assignment?.points_possible ?? null,
|
|
25
|
+
published: d.published,
|
|
26
|
+
discussion_type: d.discussion_type,
|
|
27
|
+
assignment_id: d.assignment_id,
|
|
28
|
+
html_url: d.html_url,
|
|
29
|
+
}));
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
server.tool("get_discussion", "Get full details of a discussion topic, including its message/instructions.", {
|
|
35
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
36
|
+
discussion_id: z.string().describe("Discussion topic ID"),
|
|
37
|
+
}, async ({ course_id, discussion_id }) => {
|
|
38
|
+
const discussion = await canvas(`/courses/${course_id}/discussion_topics/${discussion_id}`);
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{ type: "text", text: JSON.stringify(discussion, null, 2) },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
server.tool("create_discussion", "Create a new graded or ungraded discussion topic.", {
|
|
46
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
47
|
+
title: z.string().describe("Discussion title"),
|
|
48
|
+
message: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Discussion prompt/instructions (HTML)"),
|
|
52
|
+
discussion_type: z
|
|
53
|
+
.enum(["side_comment", "threaded"])
|
|
54
|
+
.default("threaded")
|
|
55
|
+
.describe("Comment style"),
|
|
56
|
+
published: z.boolean().default(false),
|
|
57
|
+
// Grading fields — if included, Canvas creates a linked assignment
|
|
58
|
+
points_possible: z
|
|
59
|
+
.number()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("If set, makes this a graded discussion"),
|
|
62
|
+
assignment_group_id: z.string().optional().describe("Group ID (use list_assignment_groups to find)"),
|
|
63
|
+
due_at: z.string().optional().describe("Due date (ISO 8601)"),
|
|
64
|
+
unlock_at: z.string().optional().describe("Available from (ISO 8601)"),
|
|
65
|
+
lock_at: z.string().optional().describe("Available until (ISO 8601)"),
|
|
66
|
+
}, async ({ course_id, points_possible, assignment_group_id, due_at, unlock_at, lock_at, ...params }) => {
|
|
67
|
+
const body = { ...params };
|
|
68
|
+
// If grading fields are provided, nest them under assignment
|
|
69
|
+
if (points_possible != null) {
|
|
70
|
+
body.assignment = {
|
|
71
|
+
points_possible,
|
|
72
|
+
assignment_group_id,
|
|
73
|
+
due_at,
|
|
74
|
+
unlock_at,
|
|
75
|
+
lock_at,
|
|
76
|
+
submission_types: ["discussion_topic"],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const result = await canvas(`/courses/${course_id}/discussion_topics`, { method: "POST", body: JSON.stringify(body) });
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: `Created discussion "${result.title}" (ID: ${result.id})\n${result.html_url}`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
server.tool("update_discussion", "Update a discussion topic's title, message, or published state. Cannot update dates or points — for those, use update_assignment or update_assignment_dates with the discussion's assignment_id (from list_discussions).", {
|
|
90
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
91
|
+
discussion_id: z.string().describe("Discussion topic ID"),
|
|
92
|
+
title: z.string().optional(),
|
|
93
|
+
message: z.string().optional().describe("HTML prompt/instructions"),
|
|
94
|
+
published: z.boolean().optional(),
|
|
95
|
+
}, async ({ course_id, discussion_id, ...params }) => {
|
|
96
|
+
const result = await canvas(`/courses/${course_id}/discussion_topics/${discussion_id}`, { method: "PUT", body: JSON.stringify(params) });
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: `Updated discussion "${result.title}" (ID: ${result.id})`,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
server.tool("batch_update_discussions", "Update title/message/published across multiple discussions. Cannot update dates or points — use batch_update_dates with assignment_ids for date changes on graded discussions.", {
|
|
107
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
108
|
+
discussion_ids: z.array(z.string()).describe("Discussion topic IDs"),
|
|
109
|
+
updates: z.object({
|
|
110
|
+
message: z.string().optional().describe("HTML instructions"),
|
|
111
|
+
discussion_type: z.enum(["side_comment", "threaded"]).optional(),
|
|
112
|
+
published: z.boolean().optional(),
|
|
113
|
+
}),
|
|
114
|
+
}, async ({ course_id, discussion_ids, updates }) => {
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const id of discussion_ids) {
|
|
117
|
+
try {
|
|
118
|
+
const result = await canvas(`/courses/${course_id}/discussion_topics/${id}`, { method: "PUT", body: JSON.stringify(updates) });
|
|
119
|
+
results.push(` OK: "${result.title}" (${id})`);
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
results.push(` FAILED: ${id} — ${e.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: `Batch update complete (${discussion_ids.length} discussions):\n${results.join("\n")}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { canvas, canvasAll } from "../canvas-client.js";
|
|
3
|
+
export function registerEnrollmentTools(server) {
|
|
4
|
+
server.tool("list_students", "List students enrolled in a course with enrollment-specific data (section, activity, last login). For a simpler user list with any role (teachers, TAs, etc.), use list_users_in_course instead.", {
|
|
5
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
6
|
+
section_id: z
|
|
7
|
+
.string()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe("Filter to a specific section"),
|
|
10
|
+
state: z
|
|
11
|
+
.array(z.enum(["active", "invited", "completed", "inactive"]))
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Filter by enrollment state(s)"),
|
|
14
|
+
}, async ({ course_id, section_id, state }) => {
|
|
15
|
+
const params = {
|
|
16
|
+
"type[]": "StudentEnrollment",
|
|
17
|
+
};
|
|
18
|
+
if (section_id)
|
|
19
|
+
params.section_id = section_id;
|
|
20
|
+
if (state)
|
|
21
|
+
params["state[]"] = state.join(",");
|
|
22
|
+
const enrollments = await canvasAll(`/courses/${course_id}/enrollments`, params);
|
|
23
|
+
const summary = enrollments.map((e) => ({
|
|
24
|
+
user_id: e.user_id,
|
|
25
|
+
name: e.user?.name ?? null,
|
|
26
|
+
sortable_name: e.user?.sortable_name ?? null,
|
|
27
|
+
email: e.user?.email ?? null,
|
|
28
|
+
section_id: e.course_section_id,
|
|
29
|
+
enrollment_state: e.enrollment_state,
|
|
30
|
+
last_activity_at: e.last_activity_at,
|
|
31
|
+
total_activity_time_seconds: e.total_activity_time,
|
|
32
|
+
}));
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
server.tool("list_sections", "List sections in a course. Optionally include enrolled students.", {
|
|
38
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
39
|
+
include_students: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Include student enrollments in each section"),
|
|
43
|
+
}, async ({ course_id, include_students }) => {
|
|
44
|
+
const params = {};
|
|
45
|
+
if (include_students)
|
|
46
|
+
params["include[]"] = "students";
|
|
47
|
+
const sections = await canvasAll(`/courses/${course_id}/sections`, params);
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: JSON.stringify(sections, null, 2) }],
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
server.tool("get_student_enrollments", "Get enrollment details for a specific student in a course, including grades if available.", {
|
|
53
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
54
|
+
student_id: z.string().describe("Canvas user ID of the student"),
|
|
55
|
+
}, async ({ course_id, student_id }) => {
|
|
56
|
+
const enrollments = await canvasAll(`/courses/${course_id}/enrollments`, { user_id: student_id });
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{ type: "text", text: JSON.stringify(enrollments, null, 2) },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
server.tool("list_users_in_course", "List users in a course by role (teacher, student, ta, observer). Simpler output than list_students — use list_students when you need enrollment details like section, activity time, or state.", {
|
|
64
|
+
course_id: z.string().describe("Canvas course ID"),
|
|
65
|
+
enrollment_type: z
|
|
66
|
+
.enum(["teacher", "student", "ta", "observer", "designer"])
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Filter by enrollment role"),
|
|
69
|
+
search_term: z
|
|
70
|
+
.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Filter by name or email substring"),
|
|
73
|
+
include_email: z
|
|
74
|
+
.boolean()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe("Include email addresses in results"),
|
|
77
|
+
}, async ({ course_id, enrollment_type, search_term, include_email }) => {
|
|
78
|
+
const params = {};
|
|
79
|
+
if (enrollment_type)
|
|
80
|
+
params.enrollment_type = enrollment_type;
|
|
81
|
+
if (search_term)
|
|
82
|
+
params.search_term = search_term;
|
|
83
|
+
if (include_email)
|
|
84
|
+
params["include[]"] = "email";
|
|
85
|
+
const users = await canvasAll(`/courses/${course_id}/users`, params);
|
|
86
|
+
const summary = users.map((u) => ({
|
|
87
|
+
id: u.id,
|
|
88
|
+
name: u.name,
|
|
89
|
+
sortable_name: u.sortable_name,
|
|
90
|
+
email: u.email ?? null,
|
|
91
|
+
created_at: u.created_at,
|
|
92
|
+
}));
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
server.tool("get_user_profile", "Get any user's profile information including name, email, and bio. Works for students, teachers, and any Canvas user.", {
|
|
98
|
+
user_id: z.string().describe("Canvas user ID"),
|
|
99
|
+
}, async ({ user_id }) => {
|
|
100
|
+
const profile = await canvas(`/users/${user_id}/profile`);
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: JSON.stringify(profile, null, 2) }],
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|