canvas-design-mcp 0.9.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/CLAUDE.md +200 -0
- package/DESIGN.md +288 -0
- package/PROFESSOR-INSTRUCTIONS.txt +131 -0
- package/README.md +250 -0
- package/dist/canvas-api.d.ts +24 -0
- package/dist/canvas-api.d.ts.map +1 -0
- package/dist/canvas-api.js +146 -0
- package/dist/canvas-api.js.map +1 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +22 -0
- package/dist/config.js.map +1 -0
- package/dist/design-engine.d.ts +5 -0
- package/dist/design-engine.d.ts.map +1 -0
- package/dist/design-engine.js +23 -0
- package/dist/design-engine.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +567 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/accessibility.d.ts +7 -0
- package/dist/tools/accessibility.d.ts.map +1 -0
- package/dist/tools/accessibility.js +149 -0
- package/dist/tools/accessibility.js.map +1 -0
- package/dist/tools/contrast.d.ts +2 -0
- package/dist/tools/contrast.d.ts.map +1 -0
- package/dist/tools/contrast.js +7 -0
- package/dist/tools/contrast.js.map +1 -0
- package/dist/tools/critique.d.ts +23 -0
- package/dist/tools/critique.d.ts.map +1 -0
- package/dist/tools/critique.js +194 -0
- package/dist/tools/critique.js.map +1 -0
- package/dist/tools/generate.d.ts +18 -0
- package/dist/tools/generate.d.ts.map +1 -0
- package/dist/tools/generate.js +101 -0
- package/dist/tools/generate.js.map +1 -0
- package/dist/tools/gotchas.d.ts +7 -0
- package/dist/tools/gotchas.d.ts.map +1 -0
- package/dist/tools/gotchas.js +61 -0
- package/dist/tools/gotchas.js.map +1 -0
- package/dist/tools/ingest.d.ts +44 -0
- package/dist/tools/ingest.d.ts.map +1 -0
- package/dist/tools/ingest.js +211 -0
- package/dist/tools/ingest.js.map +1 -0
- package/dist/tools/list-courses.d.ts +16 -0
- package/dist/tools/list-courses.d.ts.map +1 -0
- package/dist/tools/list-courses.js +79 -0
- package/dist/tools/list-courses.js.map +1 -0
- package/dist/tools/page-io.d.ts +19 -0
- package/dist/tools/page-io.d.ts.map +1 -0
- package/dist/tools/page-io.js +77 -0
- package/dist/tools/page-io.js.map +1 -0
- package/dist/tools/panopto.d.ts +36 -0
- package/dist/tools/panopto.d.ts.map +1 -0
- package/dist/tools/panopto.js +188 -0
- package/dist/tools/panopto.js.map +1 -0
- package/dist/tools/personas.d.ts +21 -0
- package/dist/tools/personas.d.ts.map +1 -0
- package/dist/tools/personas.js +464 -0
- package/dist/tools/personas.js.map +1 -0
- package/dist/tools/philosophy.d.ts +21 -0
- package/dist/tools/philosophy.d.ts.map +1 -0
- package/dist/tools/philosophy.js +137 -0
- package/dist/tools/philosophy.js.map +1 -0
- package/dist/tools/publish.d.ts +31 -0
- package/dist/tools/publish.d.ts.map +1 -0
- package/dist/tools/publish.js +198 -0
- package/dist/tools/publish.js.map +1 -0
- package/dist/tools/redesign.d.ts +18 -0
- package/dist/tools/redesign.d.ts.map +1 -0
- package/dist/tools/redesign.js +68 -0
- package/dist/tools/redesign.js.map +1 -0
- package/dist/tools/update-kb.d.ts +10 -0
- package/dist/tools/update-kb.d.ts.map +1 -0
- package/dist/tools/update-kb.js +93 -0
- package/dist/tools/update-kb.js.map +1 -0
- package/dist/tools/validate.d.ts +10 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +76 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/wizard.d.ts +3 -0
- package/dist/wizard.d.ts.map +1 -0
- package/dist/wizard.js +229 -0
- package/dist/wizard.js.map +1 -0
- package/docs/canvas-design-kb/00-meta/Changelog.md +42 -0
- package/docs/canvas-design-kb/00-meta/Contributing.md +58 -0
- package/docs/canvas-design-kb/00-meta/KB-Overview.md +51 -0
- package/docs/canvas-design-kb/01-canvas-rce/CSS-Inline-Strategy.md +166 -0
- package/docs/canvas-design-kb/01-canvas-rce/Canvas-Built-In-CSS-Classes.md +243 -0
- package/docs/canvas-design-kb/01-canvas-rce/Canvas-Page-Types.md +59 -0
- package/docs/canvas-design-kb/01-canvas-rce/HTML-Allowlist.md +204 -0
- package/docs/canvas-design-kb/01-canvas-rce/RCE-Limitations-and-Workarounds.md +151 -0
- package/docs/canvas-design-kb/01-canvas-rce/RCE-Overview.md +92 -0
- package/docs/canvas-design-kb/02-design-md/DESIGN-MD-Canvas-Template.md +323 -0
- package/docs/canvas-design-kb/02-design-md/DESIGN-MD-File-Structure.md +245 -0
- package/docs/canvas-design-kb/02-design-md/DESIGN-MD-Overview.md +120 -0
- package/docs/canvas-design-kb/02-design-md/DESIGN-MD-Toolchain.md +234 -0
- package/docs/canvas-design-kb/03-design-systems/Color-and-Typography.md +146 -0
- package/docs/canvas-design-kb/03-design-systems/Component-Library.md +299 -0
- package/docs/canvas-design-kb/03-design-systems/Design-System-Principles.md +99 -0
- package/docs/canvas-design-kb/04-tools/Canvas-Theme-Editor.md +40 -0
- package/docs/canvas-design-kb/04-tools/Other-Canvas-Design-Tools.md +47 -0
- package/docs/canvas-design-kb/05-patterns/Course-Home-Page.md +224 -0
- package/docs/canvas-design-kb/06-accessibility/Accessibility-Overview.md +128 -0
- package/docs/canvas-design-kb/07-resources/Inspiration-and-Showcases.md +121 -0
- package/docs/canvas-design-kb/07-resources/Official-Canvas-Links.md +54 -0
- package/docs/canvas-design-kb/README.md +58 -0
- package/docs/feature-roadmap.md +123 -0
- package/docs/installation.md +340 -0
- package/package.json +45 -0
- package/scripts/deploy-public.ps1 +68 -0
- package/src/kb/design-principles.md +45 -0
- package/src/templates/ignite-assignment-page.html +203 -0
- package/src/templates/two-column-dashboard.html +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Canvas Design Studio
|
|
2
|
+
|
|
3
|
+
**An MCP server that gives AI assistants the power to generate, validate, and maintain beautiful Canvas LMS assignment pages.**
|
|
4
|
+
|
|
5
|
+
Works in Claude Code, VS Code, ChatGPT Codex, and any MCP-compatible host. Zero mandatory cloud APIs — your Canvas API token is optional.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What It Does
|
|
10
|
+
|
|
11
|
+
| Tool | What it does |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `generate_canvas_page` | Turns a raw assignment brief into polished, Canvas-safe HTML with a hero banner, two-column layout, and brand colors |
|
|
14
|
+
| `validate_canvas_html` | Checks HTML against Canvas RCE sanitizer rules and WCAG 2.1 AA accessibility checks |
|
|
15
|
+
| `update_canvas_kb` | Fetches the current Canvas HTML allowlist directly from Canvas LMS source and reports any changes |
|
|
16
|
+
| `setup_institution` | Re-runs the setup wizard to update brand colors, Canvas URL, API token, Panopto config, and philosophy KB |
|
|
17
|
+
| `list_canvas_courses` | Lists your Canvas courses with semester filtering, student counts, and favorite pinning to help choose the right one |
|
|
18
|
+
| `publish_to_canvas` | Validates and publishes generated HTML directly to a Canvas course page — with FERPA preflight and title collision protection |
|
|
19
|
+
| `critique_canvas_page` | Scores visual design quality (0–100) with 8 structural checks, strengths, and prioritized findings |
|
|
20
|
+
| `redesign_canvas_page` | Applies mechanical fixes (font floor, hero placeholder) and returns remaining findings for the AI to address |
|
|
21
|
+
| `search_panopto_videos` | Browse or search your Panopto lecture library — titles, durations, captions status (requires Panopto API) |
|
|
22
|
+
| `embed_panopto_video` | Generate Canvas-safe Panopto embed HTML — iframe for whitelisted institutions, accessible fallback link otherwise |
|
|
23
|
+
| `fetch_panopto_captions` | Download Panopto captions, strip timestamps, save as a plain-text Markdown transcript to your local KB |
|
|
24
|
+
| `ingest_assignment_folder` | Read assignment materials from a folder (course config, brief, rubric, shell) and generate a Canvas page in one step |
|
|
25
|
+
| `get_philosophy_kb` | Load your teaching philosophy KB into the AI's context — steers tone, emphasis, and pedagogy across all tools |
|
|
26
|
+
| `update_philosophy_kb` | Add a quote, teaching insight, lecture-sourced statement, or course-specific note to your philosophy KB |
|
|
27
|
+
| `get_student_personas` | Load saved student personas — statistically grounded demographic profiles for realistic audience feedback |
|
|
28
|
+
| `generate_student_personas` | Generate 1–20 student personas using real probability distributions for race, disability, and 21 other dimensions |
|
|
29
|
+
| `load_canvas_page` | Read the most recently generated HTML page from `output/` back into context (or load a named file) |
|
|
30
|
+
| `save_canvas_page` | Write improved HTML back to `output/` — automatically backs up the previous version before overwriting |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### Option A — npm global install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g github:Ryfter/canvas-design-studio
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then add to your MCP client config:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"canvas-design-mcp": {
|
|
48
|
+
"command": "canvas-design-mcp"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Claude Desktop** — `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
55
|
+
**VS Code** — `.vscode/mcp.json` (use `"servers"` key, not `"mcpServers"`)
|
|
56
|
+
**Cursor, Kiro, LM Studio, AnythingLLM, Codex CLI** — see [docs/installation.md](docs/installation.md) for client-specific config
|
|
57
|
+
|
|
58
|
+
### Option B — Docker (no Node.js required)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Pull the image
|
|
62
|
+
docker pull ghcr.io/ryfter/canvas-design-studio:latest
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"canvas-design": {
|
|
69
|
+
"command": "docker",
|
|
70
|
+
"args": [
|
|
71
|
+
"run", "-i", "--rm",
|
|
72
|
+
"-v", "${HOME}/.canvas-design-mcp:/root/.canvas-design-mcp",
|
|
73
|
+
"ghcr.io/ryfter/canvas-design-studio:latest"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The `-v` mount gives the container access to your institution config. Run the setup wizard once on the host before using Docker (see Step 2 below).
|
|
81
|
+
|
|
82
|
+
### 2. First run
|
|
83
|
+
|
|
84
|
+
On first use, a setup wizard runs in your terminal:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
88
|
+
║ Canvas Design Studio — First Run Setup ║
|
|
89
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
90
|
+
|
|
91
|
+
Institution name: (Boise State University)
|
|
92
|
+
Primary brand color (#hex): (#0033A0)
|
|
93
|
+
Secondary / accent color (#hex): (#D64309)
|
|
94
|
+
Canvas base URL: (https://boisestate.instructure.com)
|
|
95
|
+
Canvas API token (optional — leave blank to generate HTML and paste it manually):
|
|
96
|
+
Professor email for FERPA scan allowlist (optional):
|
|
97
|
+
Favorite Canvas course IDs, comma-separated (optional):
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Config saves to `~/.canvas-design-mcp/institution.json` — survives `npx` reinstalls.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Generating a Page
|
|
105
|
+
|
|
106
|
+
Ask your AI assistant:
|
|
107
|
+
|
|
108
|
+
> "Generate a Canvas assignment page for ITM 370, Assignment 16.06 — AI Augmented Projects, Fall 2026, Dr. Rank. Brief: Students record a 5-minute video demo of their passion project and upload to YouTube."
|
|
109
|
+
|
|
110
|
+
The tool returns:
|
|
111
|
+
|
|
112
|
+
- **Canvas-safe HTML** — inline styles only, no `<script>`, no disallowed properties
|
|
113
|
+
- **Hero image prompt** — copy/paste into ChatGPT or Midjourney (1200×400px)
|
|
114
|
+
- **Filename** — `itm-370-16.06-page.html`
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Canvas HTML Rules Enforced
|
|
119
|
+
|
|
120
|
+
The validator and generator both enforce Canvas RCE constraints:
|
|
121
|
+
|
|
122
|
+
| Disallowed | Reason |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `<style>` blocks | Canvas strips them — use inline `style=""` |
|
|
125
|
+
| `<script>` | Not permitted in Canvas RCE |
|
|
126
|
+
| `box-shadow` | Stripped by sanitizer |
|
|
127
|
+
| `opacity` | Stripped — use `rgba()` instead |
|
|
128
|
+
| `gap` | Stripped in flex/grid — use `margin` on children |
|
|
129
|
+
| `filter`, `transform`, `transition`, `animation` | All stripped |
|
|
130
|
+
| `<h1>` | Reserved for Canvas page title |
|
|
131
|
+
| `<img>` without `alt` | Accessibility violation |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Ingest Workflow (Professors)
|
|
136
|
+
|
|
137
|
+
Drop three files in an `ingest/` folder and ask your AI to build the page:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
ingest/
|
|
141
|
+
├── course-config.md ← course number, name, professor, semester
|
|
142
|
+
├── assignment-brief.md ← raw assignment instructions (any format)
|
|
143
|
+
└── style-notes.md ← optional layout/tone preferences
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
> "Read everything in `ingest/`, then generate a Canvas assignment page."
|
|
147
|
+
|
|
148
|
+
The AI reads all three files, rewrites the brief into student-friendly copy, applies your brand colors, and saves the result to `output/`.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Publishing to Canvas
|
|
153
|
+
|
|
154
|
+
Publishing directly to Canvas requires a Canvas API token. Professors who prefer the manual workflow can skip this entirely — just paste the generated HTML into Canvas as normal.
|
|
155
|
+
|
|
156
|
+
### 1. List your courses
|
|
157
|
+
|
|
158
|
+
> "List my Canvas courses for this semester"
|
|
159
|
+
|
|
160
|
+
The tool returns each course with its ID, student count, teachers, and term — enough to confirm you have the right one before publishing.
|
|
161
|
+
|
|
162
|
+
### 2. Publish a page
|
|
163
|
+
|
|
164
|
+
> "Publish the generated HTML to course 12345 as 'ITM 310 — Assignment 16.06'"
|
|
165
|
+
|
|
166
|
+
Before writing to Canvas, the tool automatically:
|
|
167
|
+
|
|
168
|
+
- Scans for obvious FERPA/PII risks (student IDs, grade disclosures)
|
|
169
|
+
- Validates the HTML against Canvas RCE rules
|
|
170
|
+
- Checks for existing pages with similar titles
|
|
171
|
+
|
|
172
|
+
### 3. Handle a title collision
|
|
173
|
+
|
|
174
|
+
If a similar page already exists, the tool stops and asks how to proceed:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
A page with a similar title already exists:
|
|
178
|
+
Existing: "ITM 310 — Assignment 16.06 AI Projects"
|
|
179
|
+
New: "ITM 310 — Assignment 16.06"
|
|
180
|
+
|
|
181
|
+
Rerun publish_to_canvas with one of these options:
|
|
182
|
+
collisionAction: "update" to replace the existing page content
|
|
183
|
+
collisionAction: "create" to create a new page with this title
|
|
184
|
+
collisionAction: "related" and relatedPageTitle to create a named variation
|
|
185
|
+
collisionAction: "cancel" to stop
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Canvas keeps full page revision history, so an update is always reversible. Use `skipFerpaCheck: true` or `forcePublish: true` only after reviewing the warning they describe.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Keeping the KB Current
|
|
193
|
+
|
|
194
|
+
Canvas occasionally updates its HTML allowlist. Run:
|
|
195
|
+
|
|
196
|
+
> "Update the Canvas knowledge base"
|
|
197
|
+
|
|
198
|
+
The tool fetches `canvas_sanitize.rb` directly from the Canvas LMS GitHub source and reports additions or removals. Results are cached for 24 hours; pass `force: true` to refresh immediately.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Configuration
|
|
203
|
+
|
|
204
|
+
Config file: `~/.canvas-design-mcp/institution.json`
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"institution": "Boise State University",
|
|
209
|
+
"colors": {
|
|
210
|
+
"primary": "#0033A0",
|
|
211
|
+
"primaryDark": "#002277",
|
|
212
|
+
"primaryLight": "#E6ECF9",
|
|
213
|
+
"secondary": "#D64309"
|
|
214
|
+
},
|
|
215
|
+
"canvasUrl": "https://boisestate.instructure.com",
|
|
216
|
+
"apiToken": "",
|
|
217
|
+
"professorEmail": "you@university.edu",
|
|
218
|
+
"favoriteCourses": [12345, 67890],
|
|
219
|
+
"kbTipShown": false
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`apiToken`, `professorEmail`, and `favoriteCourses` are optional. The generate and validate tools work without an API token. Run `setup_institution` to update any field interactively.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Requirements
|
|
228
|
+
|
|
229
|
+
- Node.js 18 or later
|
|
230
|
+
- Any MCP-compatible AI host (Claude Code, VS Code Copilot, ChatGPT Codex, etc.)
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Roadmap
|
|
235
|
+
|
|
236
|
+
- **v0.1** — Core MCP server: generate, validate, KB refresh, institution setup ✓
|
|
237
|
+
- **v0.2** — Direct Canvas publishing: `list_canvas_courses` + `publish_to_canvas` ✓
|
|
238
|
+
- **v0.3** — Accessibility module (WCAG 2.1 AA checks in wizard, validator, and generator) ✓
|
|
239
|
+
- **v0.4** — Design Intelligence Brain (critique + redesign suggestions via host AI) ✓
|
|
240
|
+
- **v0.5** — Panopto video integration (search, embed, caption download) ✓
|
|
241
|
+
- **v0.6** — Assignment folder ingest (drop files in a folder, get a page) ✓
|
|
242
|
+
- **v0.7** — Professor philosophy KB (steering context for every page Claude generates) ✓
|
|
243
|
+
- **v0.8** — Student persona review (statistically grounded audience feedback before publishing) ✓
|
|
244
|
+
- **v0.9** — Assignment improvement loop (load page from output/, apply critique, save back with backup) ✓
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CanvasCourse, CanvasPage, InstitutionConfig } from './types.js';
|
|
2
|
+
export interface CanvasApiClientOptions {
|
|
3
|
+
retryDelaysMs?: number[];
|
|
4
|
+
}
|
|
5
|
+
export declare class CanvasApiError extends Error {
|
|
6
|
+
code: string;
|
|
7
|
+
status: number;
|
|
8
|
+
details?: unknown;
|
|
9
|
+
constructor(status: number, code: string, message: string, details?: unknown);
|
|
10
|
+
}
|
|
11
|
+
export declare class CanvasApiClient {
|
|
12
|
+
private readonly config;
|
|
13
|
+
private readonly retryDelaysMs;
|
|
14
|
+
constructor(config: InstitutionConfig, options?: CanvasApiClientOptions);
|
|
15
|
+
listCourses(enrollmentWorkflowStates?: string | string[]): Promise<CanvasCourse[]>;
|
|
16
|
+
listPages(courseId: number): Promise<CanvasPage[]>;
|
|
17
|
+
createPage(courseId: number, title: string, html: string): Promise<CanvasPage>;
|
|
18
|
+
updatePage(courseId: number, pageUrl: string, html: string): Promise<CanvasPage>;
|
|
19
|
+
private paginatedGet;
|
|
20
|
+
private request;
|
|
21
|
+
private headers;
|
|
22
|
+
private fetchWithRetry;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=canvas-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-api.d.ts","sourceRoot":"","sources":["../src/canvas-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAK9E,MAAM,WAAW,sBAAsB;IACrC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,qBAAa,cAAe,SAAQ,KAAK;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;gBAEN,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAO7E;AAqED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAW;gBAE7B,MAAM,EAAE,iBAAiB,EAAE,OAAO,GAAE,sBAA2B;IAKrE,WAAW,CAAC,wBAAwB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAQlF,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAIlD,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAM9E,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;YAMxE,YAAY;YAcZ,OAAO;IASrB,OAAO,CAAC,OAAO;YAQD,cAAc;CAsB7B"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export class CanvasApiError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
status;
|
|
4
|
+
details;
|
|
5
|
+
constructor(status, code, message, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'CanvasApiError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.details = details;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function trimTrailingSlash(value) {
|
|
14
|
+
return value.replace(/\/+$/, '');
|
|
15
|
+
}
|
|
16
|
+
function appendQueryValue(url, key, value) {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return;
|
|
19
|
+
const values = Array.isArray(value) ? value : [value];
|
|
20
|
+
for (const item of values) {
|
|
21
|
+
url.searchParams.append(key, item);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function joinApiUrl(baseUrl, path, params) {
|
|
25
|
+
const url = new URL(`${trimTrailingSlash(baseUrl)}/api/v1/${path.replace(/^\/+/, '')}`);
|
|
26
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
27
|
+
appendQueryValue(url, key, value);
|
|
28
|
+
}
|
|
29
|
+
return url.toString();
|
|
30
|
+
}
|
|
31
|
+
function parseNextLink(linkHeader) {
|
|
32
|
+
if (!linkHeader)
|
|
33
|
+
return undefined;
|
|
34
|
+
const next = linkHeader.split(',').find(part => part.includes('rel="next"'));
|
|
35
|
+
const match = next?.match(/<([^>]+)>/);
|
|
36
|
+
return match?.[1];
|
|
37
|
+
}
|
|
38
|
+
function sleep(ms) {
|
|
39
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
40
|
+
}
|
|
41
|
+
async function responseDetails(response) {
|
|
42
|
+
try {
|
|
43
|
+
return await response.json();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
try {
|
|
47
|
+
return await response.text();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function mapError(status, details) {
|
|
55
|
+
if (status === 401) {
|
|
56
|
+
return new CanvasApiError(status, 'CANVAS_UNAUTHORIZED', 'Canvas rejected the API token. Run setup_institution to update it.', details);
|
|
57
|
+
}
|
|
58
|
+
if (status === 403) {
|
|
59
|
+
return new CanvasApiError(status, 'CANVAS_FORBIDDEN', 'Your Canvas API token or Canvas role does not allow editing pages in this course.', details);
|
|
60
|
+
}
|
|
61
|
+
if (status === 404) {
|
|
62
|
+
return new CanvasApiError(status, 'CANVAS_NOT_FOUND', 'Course or page not found. It may have been deleted, concluded, or unavailable to your Canvas role.', details);
|
|
63
|
+
}
|
|
64
|
+
if (status === 422) {
|
|
65
|
+
return new CanvasApiError(status, 'CANVAS_REJECTED_HTML', 'Canvas rejected the page content. Review the Canvas response details.', details);
|
|
66
|
+
}
|
|
67
|
+
if (status === 429) {
|
|
68
|
+
return new CanvasApiError(status, 'CANVAS_RATE_LIMITED', 'Canvas is rate limiting requests. Try again in a few minutes.', details);
|
|
69
|
+
}
|
|
70
|
+
return new CanvasApiError(status, 'CANVAS_HTTP_ERROR', `Canvas API returned HTTP ${status}.`, details);
|
|
71
|
+
}
|
|
72
|
+
export class CanvasApiClient {
|
|
73
|
+
config;
|
|
74
|
+
retryDelaysMs;
|
|
75
|
+
constructor(config, options = {}) {
|
|
76
|
+
this.config = config;
|
|
77
|
+
this.retryDelaysMs = options.retryDelaysMs ?? [2000, 4000, 8000];
|
|
78
|
+
}
|
|
79
|
+
async listCourses(enrollmentWorkflowStates) {
|
|
80
|
+
return this.paginatedGet('courses', {
|
|
81
|
+
per_page: '50',
|
|
82
|
+
'include[]': ['term', 'total_students', 'teachers'],
|
|
83
|
+
'enrollment_workflow_state[]': enrollmentWorkflowStates,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async listPages(courseId) {
|
|
87
|
+
return this.paginatedGet(`courses/${courseId}/pages`, { per_page: '50' });
|
|
88
|
+
}
|
|
89
|
+
async createPage(courseId, title, html) {
|
|
90
|
+
return this.request('POST', `courses/${courseId}/pages`, {
|
|
91
|
+
wiki_page: { title, body: html, published: true },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async updatePage(courseId, pageUrl, html) {
|
|
95
|
+
return this.request('PUT', `courses/${courseId}/pages/${encodeURIComponent(pageUrl)}`, {
|
|
96
|
+
wiki_page: { body: html },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async paginatedGet(path, params) {
|
|
100
|
+
let nextUrl = joinApiUrl(this.config.canvasUrl, path, params);
|
|
101
|
+
const all = [];
|
|
102
|
+
while (nextUrl) {
|
|
103
|
+
const response = await this.fetchWithRetry(nextUrl, { method: 'GET', headers: this.headers() });
|
|
104
|
+
const body = await response.json();
|
|
105
|
+
all.push(...body);
|
|
106
|
+
nextUrl = parseNextLink(response.headers.get('link'));
|
|
107
|
+
}
|
|
108
|
+
return all;
|
|
109
|
+
}
|
|
110
|
+
async request(method, path, body) {
|
|
111
|
+
const response = await this.fetchWithRetry(joinApiUrl(this.config.canvasUrl, path), {
|
|
112
|
+
method,
|
|
113
|
+
headers: this.headers(),
|
|
114
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
115
|
+
});
|
|
116
|
+
return response.json();
|
|
117
|
+
}
|
|
118
|
+
headers() {
|
|
119
|
+
return {
|
|
120
|
+
Authorization: `Bearer ${this.config.apiToken ?? ''}`,
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
Accept: 'application/json',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async fetchWithRetry(url, init) {
|
|
126
|
+
for (let attempt = 0; attempt <= this.retryDelaysMs.length; attempt += 1) {
|
|
127
|
+
let response;
|
|
128
|
+
try {
|
|
129
|
+
response = await fetch(url, init);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
throw new CanvasApiError(0, 'CANVAS_NETWORK_ERROR', 'Canvas API unreachable - check your Canvas URL in institution config and try again.', err);
|
|
133
|
+
}
|
|
134
|
+
if (response.ok)
|
|
135
|
+
return response;
|
|
136
|
+
const details = await responseDetails(response);
|
|
137
|
+
if (response.status === 429 && attempt < this.retryDelaysMs.length) {
|
|
138
|
+
await sleep(this.retryDelaysMs[attempt]);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
throw mapError(response.status, details);
|
|
142
|
+
}
|
|
143
|
+
throw new CanvasApiError(429, 'CANVAS_RATE_LIMITED', 'Canvas is rate limiting requests. Try again in a few minutes.');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=canvas-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-api.js","sourceRoot":"","sources":["../src/canvas-api.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { InstitutionConfig } from './types.js';
|
|
2
|
+
export declare function configExists(): boolean;
|
|
3
|
+
export declare function loadConfig(): InstitutionConfig;
|
|
4
|
+
export declare function saveConfig(config: InstitutionConfig): void;
|
|
5
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKpD,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED,wBAAgB,UAAU,IAAI,iBAAiB,CAM9C;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAK1D"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
const CONFIG_DIR = join(homedir(), '.canvas-design-mcp');
|
|
5
|
+
const CONFIG_PATH = join(CONFIG_DIR, 'institution.json');
|
|
6
|
+
export function configExists() {
|
|
7
|
+
return existsSync(CONFIG_PATH);
|
|
8
|
+
}
|
|
9
|
+
export function loadConfig() {
|
|
10
|
+
if (!configExists()) {
|
|
11
|
+
throw new Error(`No institution config found at ${CONFIG_PATH}. Run setup_institution first.`);
|
|
12
|
+
}
|
|
13
|
+
const raw = readFileSync(CONFIG_PATH, 'utf-8');
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
export function saveConfig(config) {
|
|
17
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
18
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAGxE,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,oBAAoB,CAAC,CAAC;AACzD,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;AAEzD,MAAM,UAAU,YAAY;IAC1B,OAAO,UAAU,CAAC,WAAW,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,kCAAkC,WAAW,gCAAgC,CAAC,CAAC;IACjG,CAAC;IACD,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAsB,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAyB;IAClD,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AACvE,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { InstitutionConfig } from './types.js';
|
|
2
|
+
export declare function resolveTokens(template: string, config: InstitutionConfig): string;
|
|
3
|
+
export declare function loadTemplate(name: string): string;
|
|
4
|
+
export declare function applyTemplate(templateName: string, config: InstitutionConfig): string;
|
|
5
|
+
//# sourceMappingURL=design-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-engine.d.ts","sourceRoot":"","sources":["../src/design-engine.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAIpD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAYjF;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAGrF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
export function resolveTokens(template, config) {
|
|
6
|
+
const tokens = {
|
|
7
|
+
'{{institution.name}}': config.institution,
|
|
8
|
+
'{{colors.primary}}': config.colors.primary,
|
|
9
|
+
'{{colors.primaryDark}}': config.colors.primaryDark,
|
|
10
|
+
'{{colors.primaryLight}}': config.colors.primaryLight,
|
|
11
|
+
'{{colors.secondary}}': config.colors.secondary,
|
|
12
|
+
};
|
|
13
|
+
return Object.entries(tokens).reduce((html, [token, value]) => html.replaceAll(token, value), template);
|
|
14
|
+
}
|
|
15
|
+
export function loadTemplate(name) {
|
|
16
|
+
const templatePath = join(__dirname, 'templates', `${name}.html`);
|
|
17
|
+
return readFileSync(templatePath, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
export function applyTemplate(templateName, config) {
|
|
20
|
+
const template = loadTemplate(templateName);
|
|
21
|
+
return resolveTokens(template, config);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=design-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-engine.js","sourceRoot":"","sources":["../src/design-engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAGpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,MAAyB;IACvE,MAAM,MAAM,GAA2B;QACrC,sBAAsB,EAAE,MAAM,CAAC,WAAW;QAC1C,oBAAoB,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO;QAC3C,wBAAwB,EAAE,MAAM,CAAC,MAAM,CAAC,WAAW;QACnD,yBAAyB,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY;QACrD,sBAAsB,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS;KAChD,CAAC;IACF,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAClC,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,EACvD,QAAQ,CACT,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;IAClE,OAAO,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,YAAoB,EAAE,MAAyB;IAC3E,MAAM,QAAQ,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IAC5C,OAAO,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|