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
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Canvas Agent
2
+
3
+ MCP server that connects Claude AI to Instructure Canvas LMS. Manage courses, assignments, grades, and more through natural language.
4
+
5
+ ## Quick Setup
6
+
7
+ Full setup guide: **[hughsibbele.github.io/Canvas-Agent](https://hughsibbele.github.io/Canvas-Agent)**
8
+
9
+ If you already have Claude Code and Node.js installed:
10
+
11
+ ```bash
12
+ npx -y canvas-agent setup
13
+ ```
14
+
15
+ The wizard will walk you through connecting your Canvas account.
16
+
17
+ ## What It Does
18
+
19
+ Canvas Agent gives Claude access to your Canvas LMS:
20
+
21
+ - **Courses & Modules** — list, organize, and manage course structure
22
+ - **Assignments** — create, update, set due dates and submission types
23
+ - **Grading & Rubrics** — grade submissions, create rubrics, post grades
24
+ - **Discussions & Quizzes** — create discussion boards and quizzes
25
+ - **Student Management** — enrollments, submissions, analytics
26
+ - **Pages, Files & Calendar** — create pages, upload files, manage events
27
+
28
+ ## Development
29
+
30
+ ```bash
31
+ git clone https://github.com/hughsibbele/Canvas-Agent.git
32
+ cd Canvas-Agent
33
+ npm install
34
+ cp .env.example .env # add your Canvas URL and API token
35
+ npm run build
36
+ npm start
37
+ ```
38
+
39
+ ## License
40
+
41
+ MIT
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Thin wrapper around the Canvas REST API with automatic pagination.
3
+ */
4
+ /** Single API call — returns parsed JSON. */
5
+ export declare function canvas(path: string, options?: RequestInit): Promise<any>;
6
+ /**
7
+ * Auto-paginated GET — follows Link: <...>; rel="next" headers
8
+ * and returns all results as a flat array.
9
+ * Canvas defaults to 10 items per page; we request 100.
10
+ */
11
+ export declare function canvasAll(path: string, params?: Record<string, string>): Promise<any[]>;
12
+ /** Summarize an assignment/discussion/quiz to reduce token usage. */
13
+ export declare function summarizeItem(item: any): {
14
+ id: any;
15
+ name: any;
16
+ due_at: any;
17
+ unlock_at: any;
18
+ lock_at: any;
19
+ points_possible: any;
20
+ submission_types: any;
21
+ published: any;
22
+ assignment_group_id: any;
23
+ html_url: any;
24
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Thin wrapper around the Canvas REST API with automatic pagination.
3
+ */
4
+ const BASE_URL = process.env.CANVAS_API_URL;
5
+ const TOKEN = process.env.CANVAS_API_TOKEN;
6
+ if (!BASE_URL || !TOKEN) {
7
+ throw new Error("Missing CANVAS_API_URL or CANVAS_API_TOKEN environment variables.\n" +
8
+ "Copy .env.example to .env and fill in your values.");
9
+ }
10
+ function authHeaders(extra) {
11
+ return {
12
+ Authorization: `Bearer ${TOKEN}`,
13
+ "Content-Type": "application/json",
14
+ ...extra,
15
+ };
16
+ }
17
+ /** Parse Canvas Link header to find the "next" page URL. */
18
+ function getNextUrl(linkHeader) {
19
+ if (!linkHeader)
20
+ return null;
21
+ const match = linkHeader.match(/<([^>]+)>;\s*rel="next"/);
22
+ return match ? match[1] : null;
23
+ }
24
+ /** Retry a fetch after a delay (for rate-limit backoff). */
25
+ async function fetchWithRetry(url, init, retries = 3) {
26
+ for (let attempt = 0; attempt <= retries; attempt++) {
27
+ const res = await fetch(url, init);
28
+ if (res.status === 429 && attempt < retries) {
29
+ const retryAfter = res.headers.get("retry-after");
30
+ const delay = retryAfter ? Number(retryAfter) * 1000 : 1000 * (attempt + 1);
31
+ await new Promise((r) => setTimeout(r, delay));
32
+ continue;
33
+ }
34
+ return res;
35
+ }
36
+ throw new Error("Unreachable");
37
+ }
38
+ /** Single API call — returns parsed JSON. */
39
+ export async function canvas(path, options) {
40
+ const url = path.startsWith("http") ? path : `${BASE_URL}${path}`;
41
+ const res = await fetchWithRetry(url, {
42
+ ...options,
43
+ headers: authHeaders(options?.headers),
44
+ });
45
+ if (!res.ok) {
46
+ const body = await res.text();
47
+ throw new Error(`Canvas API ${res.status} ${res.statusText}: ${body}`);
48
+ }
49
+ // Some endpoints (DELETE) return 204 with no body
50
+ if (res.status === 204)
51
+ return { success: true };
52
+ return res.json();
53
+ }
54
+ /**
55
+ * Auto-paginated GET — follows Link: <...>; rel="next" headers
56
+ * and returns all results as a flat array.
57
+ * Canvas defaults to 10 items per page; we request 100.
58
+ */
59
+ export async function canvasAll(path, params) {
60
+ const sep = path.includes("?") ? "&" : "?";
61
+ const qs = new URLSearchParams({ per_page: "100", ...params }).toString();
62
+ let url = `${BASE_URL}${path}${sep}${qs}`;
63
+ const results = [];
64
+ while (url) {
65
+ const res = await fetchWithRetry(url, { headers: authHeaders() });
66
+ if (!res.ok) {
67
+ const body = await res.text();
68
+ throw new Error(`Canvas API ${res.status}: ${body}`);
69
+ }
70
+ const data = await res.json();
71
+ results.push(...(Array.isArray(data) ? data : [data]));
72
+ url = getNextUrl(res.headers.get("link"));
73
+ }
74
+ return results;
75
+ }
76
+ /** Summarize an assignment/discussion/quiz to reduce token usage. */
77
+ export function summarizeItem(item) {
78
+ return {
79
+ id: item.id,
80
+ name: item.name ?? item.title,
81
+ due_at: item.due_at,
82
+ unlock_at: item.unlock_at,
83
+ lock_at: item.lock_at,
84
+ points_possible: item.points_possible,
85
+ submission_types: item.submission_types,
86
+ published: item.published,
87
+ assignment_group_id: item.assignment_group_id,
88
+ html_url: item.html_url,
89
+ };
90
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ const command = process.argv[2];
3
+ if (command === "setup") {
4
+ const { runSetup } = await import("./setup.js");
5
+ await runSetup();
6
+ }
7
+ else {
8
+ await import("./index.js");
9
+ }
10
+ export {};
@@ -0,0 +1 @@
1
+ import "dotenv/config";
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ import "dotenv/config";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerCourseTools } from "./tools/courses.js";
5
+ import { registerAssignmentTools } from "./tools/assignments.js";
6
+ import { registerDiscussionTools } from "./tools/discussions.js";
7
+ import { registerQuizTools } from "./tools/quizzes.js";
8
+ import { registerSchedulingTools } from "./tools/scheduling.js";
9
+ import { registerSubmissionTools } from "./tools/submissions.js";
10
+ import { registerRubricTools } from "./tools/rubrics.js";
11
+ import { registerNewQuizTools } from "./tools/new-quizzes.js";
12
+ import { registerGradingTools } from "./tools/grading.js";
13
+ import { registerPageTools } from "./tools/pages.js";
14
+ import { registerEnrollmentTools } from "./tools/enrollments.js";
15
+ import { registerAnalyticsTools } from "./tools/analytics.js";
16
+ import { registerCalendarTools } from "./tools/calendar.js";
17
+ import { registerFileTools } from "./tools/files.js";
18
+ import { registerModuleTools } from "./tools/modules.js";
19
+ const server = new McpServer({
20
+ name: "canvas-agent",
21
+ version: "1.0.0",
22
+ });
23
+ // Register all tool groups
24
+ registerCourseTools(server);
25
+ registerAssignmentTools(server);
26
+ registerDiscussionTools(server);
27
+ registerQuizTools(server);
28
+ registerSchedulingTools(server);
29
+ registerSubmissionTools(server);
30
+ registerRubricTools(server);
31
+ registerNewQuizTools(server);
32
+ registerGradingTools(server);
33
+ registerPageTools(server);
34
+ registerEnrollmentTools(server);
35
+ registerAnalyticsTools(server);
36
+ registerCalendarTools(server);
37
+ registerFileTools(server);
38
+ registerModuleTools(server);
39
+ // Connect via stdio (Claude Code launches this as a subprocess)
40
+ const transport = new StdioServerTransport();
41
+ await server.connect(transport);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Interactive setup wizard for Canvas Agent.
3
+ * Guides non-technical users through connecting Canvas to Claude.
4
+ * Uses only Node.js built-ins — no external dependencies.
5
+ */
6
+ export declare function runSetup(): Promise<void>;
package/dist/setup.js ADDED
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Interactive setup wizard for Canvas Agent.
3
+ * Guides non-technical users through connecting Canvas to Claude.
4
+ * Uses only Node.js built-ins — no external dependencies.
5
+ */
6
+ import { createInterface } from "readline/promises";
7
+ import { execSync } from "child_process";
8
+ import { stdin, stdout, platform } from "process";
9
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
10
+ import { join } from "path";
11
+ import { homedir } from "os";
12
+ // ANSI color helpers
13
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
14
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
15
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
16
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
17
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
18
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
19
+ function banner() {
20
+ console.log();
21
+ console.log(cyan(" ╔══════════════════════════════════════╗"));
22
+ console.log(cyan(" ║") + bold(" Canvas Agent — Setup Wizard ") + cyan("║"));
23
+ console.log(cyan(" ╚══════════════════════════════════════╝"));
24
+ console.log();
25
+ console.log(" This will connect Claude to your Canvas courses.");
26
+ console.log(" You'll need about 3 minutes and access to your");
27
+ console.log(" Canvas account.\n");
28
+ }
29
+ function isClaudeInstalled() {
30
+ try {
31
+ execSync("claude --version", { stdio: "pipe" });
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ function openBrowser(url) {
39
+ try {
40
+ const cmd = platform === "darwin"
41
+ ? "open"
42
+ : platform === "win32"
43
+ ? "start"
44
+ : "xdg-open";
45
+ execSync(`${cmd} "${url}"`, { stdio: "ignore" });
46
+ }
47
+ catch {
48
+ // Silently fail — we print the URL as fallback
49
+ }
50
+ }
51
+ function normalizeCanvasUrl(raw) {
52
+ let hostname = raw.trim();
53
+ // Strip protocol
54
+ hostname = hostname.replace(/^https?:\/\//, "");
55
+ // Strip paths
56
+ hostname = hostname.replace(/\/.*$/, "");
57
+ // Strip port for validation but keep it
58
+ const apiUrl = `https://${hostname}/api/v1`;
59
+ return { hostname, apiUrl };
60
+ }
61
+ async function validateCredentials(apiUrl, token) {
62
+ try {
63
+ const res = await fetch(`${apiUrl}/users/self`, {
64
+ headers: { Authorization: `Bearer ${token}` },
65
+ });
66
+ if (res.ok) {
67
+ const user = (await res.json());
68
+ return { valid: true, name: user.name };
69
+ }
70
+ if (res.status === 401) {
71
+ return { valid: false, error: "Invalid token — double-check that you copied the full token." };
72
+ }
73
+ return { valid: false, error: `Canvas returned an error (${res.status} ${res.statusText}).` };
74
+ }
75
+ catch (e) {
76
+ if (e.cause?.code === "ENOTFOUND") {
77
+ return { valid: false, error: `Could not reach "${apiUrl}" — check your Canvas address.` };
78
+ }
79
+ return { valid: false, error: e.message };
80
+ }
81
+ }
82
+ function registerWithClaudeCode(apiUrl, token) {
83
+ try {
84
+ execSync(`claude mcp add -s user -e "CANVAS_API_URL=${apiUrl}" -e "CANVAS_API_TOKEN=${token}" canvas-agent -- npx -y canvas-agent`, { stdio: "inherit" });
85
+ return true;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ function getDesktopConfigPath() {
92
+ if (platform === "darwin") {
93
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
94
+ }
95
+ if (platform === "win32" && process.env.APPDATA) {
96
+ return join(process.env.APPDATA, "Claude", "claude_desktop_config.json");
97
+ }
98
+ return null;
99
+ }
100
+ function registerWithDesktop(apiUrl, token) {
101
+ const configPath = getDesktopConfigPath();
102
+ if (!configPath)
103
+ return false;
104
+ try {
105
+ // Read existing config or start fresh
106
+ let config = {};
107
+ if (existsSync(configPath)) {
108
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
109
+ }
110
+ else {
111
+ // Ensure parent directory exists
112
+ const dir = join(configPath, "..");
113
+ mkdirSync(dir, { recursive: true });
114
+ }
115
+ // Merge in our MCP server entry
116
+ if (!config.mcpServers)
117
+ config.mcpServers = {};
118
+ config.mcpServers["canvas-agent"] = {
119
+ command: "npx",
120
+ args: ["-y", "canvas-agent"],
121
+ env: {
122
+ CANVAS_API_URL: apiUrl,
123
+ CANVAS_API_TOKEN: token,
124
+ },
125
+ };
126
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
127
+ return true;
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
133
+ function printManualConfig(apiUrl, token) {
134
+ console.log(yellow("\n Add this to your Claude MCP configuration:\n"));
135
+ const config = {
136
+ "canvas-agent": {
137
+ command: "npx",
138
+ args: ["-y", "canvas-agent"],
139
+ env: {
140
+ CANVAS_API_URL: apiUrl,
141
+ CANVAS_API_TOKEN: token,
142
+ },
143
+ },
144
+ };
145
+ console.log(" " + JSON.stringify(config, null, 2).replace(/\n/g, "\n "));
146
+ console.log();
147
+ }
148
+ export async function runSetup() {
149
+ const rl = createInterface({ input: stdin, output: stdout });
150
+ try {
151
+ banner();
152
+ // ── Step 1: Check for Claude Code ──
153
+ const hasClaude = isClaudeInstalled();
154
+ let useDesktop = false;
155
+ if (hasClaude) {
156
+ console.log(green(" ✓") + " Claude Code is installed.\n");
157
+ }
158
+ else {
159
+ console.log(yellow(" ⚠") + " Claude Code not found.\n");
160
+ console.log(" Canvas Agent works best with Claude Code.");
161
+ console.log(" To install it, run:\n");
162
+ console.log(bold(" npm install -g @anthropic-ai/claude-code\n"));
163
+ console.log(" Then run this setup again.\n");
164
+ const answer = await rl.question(" Continue with Claude Desktop setup instead? (y/n): ");
165
+ if (answer.trim().toLowerCase() !== "y") {
166
+ console.log("\n No problem! Install Claude Code and run this again:");
167
+ console.log(bold(" npx -y canvas-agent setup\n"));
168
+ return;
169
+ }
170
+ useDesktop = true;
171
+ console.log();
172
+ }
173
+ // ── Step 2: Get Canvas URL ──
174
+ console.log(bold(" Step 1: Your School's Canvas\n"));
175
+ let hostname = "";
176
+ let apiUrl = "";
177
+ while (true) {
178
+ const rawUrl = await rl.question(" Your Canvas address (e.g., myschool.instructure.com): ");
179
+ if (!rawUrl.trim())
180
+ continue;
181
+ const normalized = normalizeCanvasUrl(rawUrl);
182
+ hostname = normalized.hostname;
183
+ apiUrl = normalized.apiUrl;
184
+ if (!hostname.includes(".")) {
185
+ console.log(red(" ✗") + ` That doesn't look like a web address. Try again.\n`);
186
+ continue;
187
+ }
188
+ console.log(dim(` → ${apiUrl}`));
189
+ break;
190
+ }
191
+ // ── Step 3: Get Canvas API Token ──
192
+ console.log(bold("\n Step 2: Canvas API Token\n"));
193
+ console.log(" We need an access token from Canvas. Here's how:\n");
194
+ console.log(` 1. Log in to Canvas at ${cyan(`https://${hostname}`)}`);
195
+ console.log(" 2. Click your profile picture (top left) → " + bold("Settings"));
196
+ console.log(" 3. Scroll down to " + bold('"Approved Integrations"'));
197
+ console.log(" 4. Click " + bold('"+ New Access Token"'));
198
+ console.log(" 5. For Purpose, type: " + dim("Canvas Agent"));
199
+ console.log(" 6. Click " + bold('"Generate Token"') + " and copy the token shown\n");
200
+ const settingsUrl = `https://${hostname}/profile/settings`;
201
+ console.log(` Opening ${cyan(settingsUrl)} in your browser...`);
202
+ openBrowser(settingsUrl);
203
+ console.log();
204
+ let token = "";
205
+ while (true) {
206
+ token = (await rl.question(" Paste your token here: ")).trim();
207
+ if (!token)
208
+ continue;
209
+ // ── Step 4: Validate ──
210
+ console.log(dim(" Checking..."));
211
+ const result = await validateCredentials(apiUrl, token);
212
+ if (result.valid) {
213
+ console.log(green(" ✓") + ` Connected! Welcome, ${bold(result.name || "there")}.\n`);
214
+ break;
215
+ }
216
+ else {
217
+ console.log(red(" ✗") + ` ${result.error}\n`);
218
+ const retry = await rl.question(" Try again? (y/n): ");
219
+ if (retry.trim().toLowerCase() !== "y") {
220
+ console.log("\n No worries — run this wizard again when you're ready:");
221
+ console.log(bold(" npx -y canvas-agent setup\n"));
222
+ return;
223
+ }
224
+ // Let them fix the URL too
225
+ const fixUrl = await rl.question(" Change Canvas address too? (y/n): ");
226
+ if (fixUrl.trim().toLowerCase() === "y") {
227
+ const rawUrl = await rl.question(" Canvas address: ");
228
+ const normalized = normalizeCanvasUrl(rawUrl);
229
+ hostname = normalized.hostname;
230
+ apiUrl = normalized.apiUrl;
231
+ console.log(dim(` → ${apiUrl}\n`));
232
+ }
233
+ console.log();
234
+ }
235
+ }
236
+ // ── Step 5: Register MCP server ──
237
+ console.log(bold(" Step 3: Connecting to Claude\n"));
238
+ let registered = false;
239
+ if (!useDesktop) {
240
+ // Try Claude Code first
241
+ console.log(" Registering Canvas Agent with Claude Code...\n");
242
+ registered = registerWithClaudeCode(apiUrl, token);
243
+ if (registered) {
244
+ console.log(green("\n ✓") + " Registered with Claude Code!\n");
245
+ }
246
+ else {
247
+ console.log(yellow("\n ⚠") + " Could not register automatically.\n");
248
+ // Fall through to Desktop or manual
249
+ useDesktop = true;
250
+ }
251
+ }
252
+ if (useDesktop && !registered) {
253
+ console.log(" Setting up Claude Desktop...");
254
+ registered = registerWithDesktop(apiUrl, token);
255
+ if (registered) {
256
+ console.log(green(" ✓") + " Configured Claude Desktop!\n");
257
+ console.log(dim(" Restart Claude Desktop for changes to take effect.\n"));
258
+ }
259
+ }
260
+ if (!registered) {
261
+ printManualConfig(apiUrl, token);
262
+ console.log(" Copy the JSON above and add it to your Claude configuration.");
263
+ console.log(" For help, visit: " + cyan("https://hughsibbele.github.io/Canvas-Agent") + "\n");
264
+ }
265
+ // ── Done ──
266
+ console.log(cyan(" ╔══════════════════════════════════════╗"));
267
+ console.log(cyan(" ║") + green(" Setup Complete! ") + cyan("║"));
268
+ console.log(cyan(" ╚══════════════════════════════════════╝"));
269
+ console.log();
270
+ if (!useDesktop) {
271
+ console.log(" To start using Canvas Agent:");
272
+ console.log(" 1. Open a terminal and type: " + bold("claude"));
273
+ console.log(' 2. Try asking: ' + dim('"List my Canvas courses"'));
274
+ }
275
+ else {
276
+ console.log(" To start using Canvas Agent:");
277
+ console.log(" 1. Restart Claude Desktop");
278
+ console.log(' 2. Try asking: ' + dim('"List my Canvas courses"'));
279
+ }
280
+ console.log();
281
+ console.log(" For help: " + cyan("https://hughsibbele.github.io/Canvas-Agent"));
282
+ console.log();
283
+ }
284
+ finally {
285
+ rl.close();
286
+ }
287
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAnalyticsTools(server: McpServer): void;
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { canvas, canvasAll } from "../canvas-client.js";
3
+ export function registerAnalyticsTools(server) {
4
+ server.tool("get_course_activity", "Get daily page views and participation analytics for a course. This returns engagement metrics, not assignment data — use list_assignments for assignment info.", {
5
+ course_id: z.string().describe("Canvas course ID"),
6
+ }, async ({ course_id }) => {
7
+ const activity = await canvas(`/courses/${course_id}/analytics/activity`);
8
+ return {
9
+ content: [{ type: "text", text: JSON.stringify(activity, null, 2) }],
10
+ };
11
+ });
12
+ server.tool("get_course_assignment_analytics", "Get aggregate statistical analytics per assignment: min/max/median scores, submission counts (on_time, late, missing). This returns statistics, not the assignments themselves — use list_assignments for that.", {
13
+ course_id: z.string().describe("Canvas course ID"),
14
+ }, async ({ course_id }) => {
15
+ const analytics = await canvas(`/courses/${course_id}/analytics/assignments`);
16
+ return {
17
+ content: [{ type: "text", text: JSON.stringify(analytics, null, 2) }],
18
+ };
19
+ });
20
+ server.tool("get_student_summaries", "Get per-student engagement analytics for a course: page views, participations, and tardiness breakdown. For enrollment/roster data, use list_students instead.", {
21
+ course_id: z.string().describe("Canvas course ID"),
22
+ sort_column: z
23
+ .enum([
24
+ "name",
25
+ "name_descending",
26
+ "score",
27
+ "score_descending",
28
+ "participations",
29
+ "page_views",
30
+ ])
31
+ .optional()
32
+ .describe("Column to sort by"),
33
+ }, async ({ course_id, sort_column }) => {
34
+ const params = {};
35
+ if (sort_column)
36
+ params.sort_column = sort_column;
37
+ const summaries = await canvasAll(`/courses/${course_id}/analytics/student_summaries`, params);
38
+ return {
39
+ content: [{ type: "text", text: JSON.stringify(summaries, null, 2) }],
40
+ };
41
+ });
42
+ server.tool("get_student_activity", "Get hourly page view breakdown for a specific student in a course.", {
43
+ course_id: z.string().describe("Canvas course ID"),
44
+ student_id: z.string().describe("Canvas user ID of the student"),
45
+ }, async ({ course_id, student_id }) => {
46
+ const activity = await canvas(`/courses/${course_id}/analytics/users/${student_id}/activity`);
47
+ return {
48
+ content: [{ type: "text", text: JSON.stringify(activity, null, 2) }],
49
+ };
50
+ });
51
+ server.tool("get_student_assignment_data", "Get per-assignment scores, submission status, and timestamps for a specific student. This is analytics data — for actual submission details, use list_submissions.", {
52
+ course_id: z.string().describe("Canvas course ID"),
53
+ student_id: z.string().describe("Canvas user ID of the student"),
54
+ }, async ({ course_id, student_id }) => {
55
+ const data = await canvas(`/courses/${course_id}/analytics/users/${student_id}/assignments`);
56
+ return {
57
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
58
+ };
59
+ });
60
+ server.tool("get_student_messaging_data", "Get message counts between instructor and a specific student in a course.", {
61
+ course_id: z.string().describe("Canvas course ID"),
62
+ student_id: z.string().describe("Canvas user ID of the student"),
63
+ }, async ({ course_id, student_id }) => {
64
+ const data = await canvas(`/courses/${course_id}/analytics/users/${student_id}/communication`);
65
+ return {
66
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
67
+ };
68
+ });
69
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAssignmentTools(server: McpServer): void;