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
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
package/dist/cli.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -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);
|
package/dist/setup.d.ts
ADDED
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,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
|
+
}
|