a fancy canvas mcp server!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: fix the mcpconfig removal

+22 -418
-213
SETUP.md
··· 1 - # Canvas MCP Server Setup 2 - 3 - This guide explains how to set up the Canvas MCP server to work with your Canvas LMS institution(s). 4 - 5 - ## Multi-Institution Support 6 - 7 - The server supports three configuration modes: 8 - 9 - ### 1. **Global/Wildcard Configuration** (Easiest) 10 - 11 - If your Canvas instance supports OAuth applications that work across different domains, or if you're only supporting one institution: 12 - 13 - ```bash 14 - CANVAS_CLIENT_ID=your_client_id 15 - CANVAS_CLIENT_SECRET=your_client_secret 16 - ``` 17 - 18 - This will accept logins from **any Canvas domain** using the same OAuth credentials. 19 - 20 - ### 2. **Multiple Specific Institutions** 21 - 22 - If you need different OAuth credentials for different institutions: 23 - 24 - ```bash 25 - CANVAS_INSTITUTIONS='[ 26 - { 27 - "domain": "canvas.harvard.edu", 28 - "clientId": "xxx", 29 - "clientSecret": "yyy", 30 - "name": "Harvard University" 31 - }, 32 - { 33 - "domain": "canvas.mit.edu", 34 - "clientId": "aaa", 35 - "clientSecret": "bbb", 36 - "name": "MIT" 37 - } 38 - ]' 39 - ``` 40 - 41 - --- 42 - 43 - ## How to Get Canvas OAuth Credentials 44 - 45 - Each Canvas institution must register your MCP server as an OAuth application. Here's how: 46 - 47 - ### For Canvas Administrators: 48 - 49 - 1. **Go to Canvas Admin Panel** 50 - - Navigate to: Admin → Developer Keys 51 - 52 - 2. **Create a new Developer Key** 53 - - Click "+ Developer Key" → "+ API Key" 54 - 55 - 3. **Fill in the details:** 56 - - **Key Name**: Canvas MCP Server 57 - - **Owner Email**: Your email 58 - - **Redirect URIs**: `https://canvas.dunkirk.sh/api/auth/callback` 59 - - **Scopes**: Select the following: 60 - - `url:GET|/api/v1/courses` 61 - - `url:GET|/api/v1/assignments` 62 - - `url:GET|/api/v1/users/self` 63 - 64 - 4. **Save and Enable** 65 - - Copy the **Client ID** and **Client Secret** 66 - - Set the key state to "On" 67 - 68 - 5. **Provide credentials to the server** 69 - - Add the Client ID and Client Secret to your `.env` file 70 - 71 - ### For Users (Self-Service): 72 - 73 - If you don't have admin access to your Canvas instance: 74 - 75 - 1. Contact your Canvas LMS administrator 76 - 2. Ask them to register an OAuth application with the redirect URI: `https://canvas.dunkirk.sh/api/auth/callback` 77 - 3. Request the Client ID and Client Secret 78 - 4. Provide these to the Canvas MCP server administrator 79 - 80 - --- 81 - 82 - ## Canvas Cloud & Inherited Developer Keys 83 - 84 - Some Canvas Cloud institutions support **inherited developer keys** across a consortium. If your institution is part of a Canvas Cloud consortium, a single OAuth application might work across multiple domains. 85 - 86 - Ask your Canvas administrator if this is available. 87 - 88 - --- 89 - 90 - ## Environment Variables 91 - 92 - Copy `.env.example` to `.env` and fill in your values: 93 - 94 - ```bash 95 - cp .env.example .env 96 - ``` 97 - 98 - ### Required Variables: 99 - 100 - ```bash 101 - # Server 102 - PORT=3000 103 - HOST=localhost 104 - BASE_URL=https://canvas.dunkirk.sh 105 - 106 - # Encryption key (generate with: openssl rand -base64 32) 107 - ENCRYPTION_KEY=your_encryption_key_here 108 - 109 - # Canvas OAuth (choose one of the options above) 110 - CANVAS_CLIENT_ID=your_client_id 111 - CANVAS_CLIENT_SECRET=your_client_secret 112 - 113 - # Database 114 - DATABASE_PATH=./canvas-mcp.db 115 - ``` 116 - 117 - --- 118 - 119 - ## Installation 120 - 121 - ```bash 122 - # Install dependencies 123 - bun install 124 - 125 - # Run development server 126 - bun dev 127 - 128 - # Build for production 129 - bun run build 130 - 131 - # Run production server 132 - bun start 133 - ``` 134 - 135 - --- 136 - 137 - ## Usage 138 - 139 - 1. **Users visit**: `https://canvas.dunkirk.sh` 140 - 2. **Enter their Canvas domain**: e.g., `canvas.harvard.edu` 141 - 3. **Authenticate via Canvas OAuth** 142 - 4. **Receive an MCP API key** on their dashboard 143 - 5. **Configure their MCP client** with the API key 144 - 145 - --- 146 - 147 - ## MCP Client Configuration 148 - 149 - After getting an API key, users should add this to their MCP client config: 150 - 151 - ```json 152 - { 153 - "mcpServers": { 154 - "canvas": { 155 - "command": "bunx", 156 - "args": ["canvas-mcp-client"], 157 - "env": { 158 - "CANVAS_MCP_API_KEY": "cmcp_...", 159 - "CANVAS_MCP_URL": "https://canvas.dunkirk.sh" 160 - } 161 - } 162 - } 163 - } 164 - ``` 165 - 166 - --- 167 - 168 - ## Security Notes 169 - 170 - - **API keys are hashed** before storage using Argon2id 171 - - **Canvas tokens are encrypted** at rest using AES-256-GCM 172 - - **OAuth state parameters** prevent CSRF attacks 173 - - **HTTPS required** in production 174 - - **Session cookies** are HttpOnly and SameSite=Lax 175 - 176 - --- 177 - 178 - ## Deployment 179 - 180 - Deploy to any platform that supports Bun: 181 - 182 - - **Railway**: `railway up` 183 - - **Fly.io**: `fly launch` 184 - - **Docker**: See Dockerfile 185 - - **VPS**: Run with systemd or PM2 186 - 187 - Make sure to: 188 - - Set `BASE_URL` to your production domain 189 - - Use HTTPS (required for OAuth) 190 - - Set a strong `ENCRYPTION_KEY` 191 - - Configure Canvas OAuth redirect URI to your production URL 192 - 193 - --- 194 - 195 - ## Troubleshooting 196 - 197 - ### "Canvas domain is not configured" 198 - 199 - The server doesn't have OAuth credentials for that Canvas instance. Either: 200 - - Use a global wildcard configuration (`CANVAS_CLIENT_ID` + `CANVAS_CLIENT_SECRET`) 201 - - Add the specific domain to `CANVAS_INSTITUTIONS` 202 - 203 - ### "OAuth token exchange failed" 204 - 205 - - Verify the Client ID and Client Secret are correct 206 - - Check that the redirect URI in Canvas matches exactly: `https://canvas.dunkirk.sh/api/auth/callback` 207 - - Ensure the Canvas domain is correct (no `https://`, just `canvas.university.edu`) 208 - 209 - ### "Invalid API key" 210 - 211 - - The API key might have been regenerated 212 - - Copy the new API key from the dashboard 213 - - Update your MCP client configuration
-156
TEST_MCP.md
··· 1 - # Testing the MCP Server 2 - 3 - ## Method 1: Direct JSON-RPC Test (Quick) 4 - 5 - Test the MCP endpoint directly with curl: 6 - 7 - ```bash 8 - # Set your MCP token (get this from the dashboard) 9 - TOKEN="cmcp_your_token_here" 10 - 11 - # Test: List available tools 12 - curl -X POST https://canvas.bore.dunkirk.sh/mcp \ 13 - -H "Authorization: Bearer $TOKEN" \ 14 - -H "Content-Type: application/json" \ 15 - -H "Accept: application/json, text/event-stream" \ 16 - -d '{ 17 - "jsonrpc": "2.0", 18 - "id": 1, 19 - "method": "tools/list", 20 - "params": {} 21 - }' 22 - 23 - # Test: List courses 24 - curl -X POST https://canvas.bore.dunkirk.sh/mcp \ 25 - -H "Authorization: Bearer $TOKEN" \ 26 - -H "Content-Type: application/json" \ 27 - -H "Accept: application/json, text/event-stream" \ 28 - -d '{ 29 - "jsonrpc": "2.0", 30 - "id": 2, 31 - "method": "tools/call", 32 - "params": { 33 - "name": "list_courses", 34 - "arguments": { 35 - "enrollment_state": "active" 36 - } 37 - } 38 - }' 39 - ``` 40 - 41 - ## Method 2: Claude Desktop (Real Usage) 42 - 43 - 1. Get your MCP token from the dashboard 44 - 2. Open Claude Desktop config: 45 - - Mac: `~/Library/Application Support/Claude/claude_desktop_config.json` 46 - - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 47 - 48 - 3. Add this configuration: 49 - 50 - ```json 51 - { 52 - "mcpServers": { 53 - "canvas": { 54 - "url": "https://canvas.bore.dunkirk.sh/mcp", 55 - "headers": { 56 - "Authorization": "Bearer YOUR_MCP_TOKEN_HERE" 57 - } 58 - } 59 - } 60 - } 61 - ``` 62 - 63 - 4. Restart Claude Desktop 64 - 65 - 5. Test by asking Claude: 66 - - "What courses am I enrolled in?" 67 - - "What assignments do I have due this week?" 68 - - "Show me details about assignment ID 12345 in course 6789" 69 - 70 - ## Method 3: MCP Inspector (Visual Debugging) 71 - 72 - ```bash 73 - # Install MCP Inspector 74 - npm install -g @modelcontextprotocol/inspector 75 - 76 - # Create a config file 77 - cat > mcp-config.json <<EOF 78 - { 79 - "mcpServers": { 80 - "canvas": { 81 - "url": "https://canvas.bore.dunkirk.sh/mcp", 82 - "headers": { 83 - "Authorization": "Bearer YOUR_MCP_TOKEN_HERE" 84 - } 85 - } 86 - } 87 - } 88 - EOF 89 - 90 - # Run inspector 91 - mcp-inspector mcp-config.json 92 - ``` 93 - 94 - ## Expected Responses 95 - 96 - ### tools/list 97 - ```json 98 - { 99 - "jsonrpc": "2.0", 100 - "id": 1, 101 - "result": { 102 - "tools": [ 103 - { 104 - "name": "list_courses", 105 - "description": "List Canvas courses...", 106 - "inputSchema": {...} 107 - }, 108 - { 109 - "name": "search_assignments", 110 - ... 111 - }, 112 - { 113 - "name": "get_assignment", 114 - ... 115 - } 116 - ] 117 - } 118 - } 119 - ``` 120 - 121 - ### tools/call (list_courses) 122 - ```json 123 - { 124 - "jsonrpc": "2.0", 125 - "id": 2, 126 - "result": { 127 - "content": [ 128 - { 129 - "type": "text", 130 - "text": "[{\"id\": 123, \"name\": \"Biology 101\", ...}]" 131 - } 132 - ] 133 - } 134 - } 135 - ``` 136 - 137 - ## Troubleshooting 138 - 139 - **Error: "Missing session token"** 140 - - Make sure you're including the `Authorization: Bearer YOUR_TOKEN` header 141 - 142 - **Error: "Invalid or expired session token"** 143 - - Your MCP token expired or was regenerated 144 - - Get a new token from the dashboard 145 - 146 - **Error: "Not authenticated"** 147 - - The MCP token doesn't match any user in the database 148 - - Log in again via the web interface 149 - 150 - **Error: "Unknown tool"** 151 - - Check the tool name spelling (case-sensitive) 152 - - Run `tools/list` to see available tools 153 - 154 - **Error: Canvas API errors** 155 - - Your Canvas Personal Access Token may have expired (max 120 days) 156 - - Generate a new Canvas token and update via the web interface
+6 -22
src/index.ts
··· 1 - import { randomBytes } from "crypto"; 1 + import { randomBytes } from "node:crypto"; 2 2 import { $ } from "bun"; 3 - import DB from "./lib/db.js"; 4 3 import { CanvasClient } from "./lib/canvas.js"; 4 + import DB from "./lib/db.js"; 5 + import Mailer from "./lib/email.js"; 5 6 import { 6 - handleMcpRequest, 7 7 getProtectedResourceMetadata, 8 + handleMcpRequest, 8 9 } from "./lib/mcp-transport.js"; 9 - import Mailer from "./lib/email.js"; 10 10 11 - // Import HTML pages 11 + // import HTML pages 12 + import dashboardPage from "./public/dashboard.html"; 12 13 import indexPage from "./public/index.html"; 13 - import dashboardPage from "./public/dashboard.html"; 14 14 15 15 // Get git commit hash 16 16 let gitHash = "dev"; ··· 879 879 }, 880 880 }, 881 881 882 - // Admin endpoint to manually trigger cleanup 883 - "/api/admin/cleanup": { 884 - POST(req: Request) { 885 - const results = DB.runAllCleanups(); 886 - return Response.json({ 887 - success: true, 888 - removed: results, 889 - timestamp: new Date().toISOString(), 890 - }); 891 - }, 892 - }, 893 - 894 882 // Git version endpoint 895 883 "/api/version": { 896 884 GET() { ··· 920 908 921 909 // Background cleanup job - runs every 5 minutes 922 910 const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes 923 - 924 - console.log( 925 - `[Cleanup] Starting background cleanup job (interval: ${CLEANUP_INTERVAL / 1000}s)`, 926 - ); 927 911 928 912 // Run initial cleanup on startup 929 913 DB.runAllCleanups();
+2 -3
src/lib/db.ts
··· 1 1 import { Database } from "bun:sqlite"; 2 - import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; 2 + import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; 3 3 4 4 const db = new Database(process.env.DATABASE_PATH || "./canvas-mcp.db"); 5 5 ··· 10 10 } 11 11 12 12 const apiKeyCache = new Map<string, ApiKeyCacheEntry>(); 13 - const CACHE_TTL = parseInt(process.env.API_KEY_CACHE_TTL || "900000"); // 15 minutes default 13 + const CACHE_TTL = parseInt(process.env.API_KEY_CACHE_TTL || "900000", 10); // 15 minutes default 14 14 15 15 // Cache cleanup interval (runs every 5 minutes) 16 16 setInterval( ··· 630 630 usageLogs: this.cleanupOldUsageLogs(), 631 631 }; 632 632 633 - console.log("[Cleanup] Removed expired records:", results); 634 633 return results; 635 634 }, 636 635 };
+4 -21
src/public/dashboard.html
··· 374 374 .then(r => r.json()) 375 375 .then(data => { 376 376 const link = document.getElementById('git-hash-link'); 377 - link.href = `https://tangled.org/dunkirk.sh/canvas-mcp/commit/${data.hash}`; 378 - link.textContent = data.shortHash; 377 + if (link) { 378 + link.href = `https://tangled.org/dunkirk.sh/canvas-mcp/commit/${data.hash}`; 379 + link.textContent = data.shortHash; 380 + } 379 381 }) 380 382 .catch(() => { }); 381 383 ··· 413 415 renderAccountInfo(); 414 416 renderUsageStats(); 415 417 setupApiKeyDisplay(); 416 - updateMCPConfig(); 417 418 } 418 419 } catch (error) { 419 420 console.error('Failed to load dashboard:', error); ··· 462 463 `; 463 464 } 464 465 465 - function updateMCPConfig() { 466 - const token = apiKeyVisible ? userData.api_key : "YOUR_MCP_TOKEN_HERE"; 467 - const config = { 468 - mcpServers: { 469 - canvas: { 470 - url: `${window.location.origin}/mcp`, 471 - headers: { 472 - "Authorization": `Bearer ${token}` 473 - } 474 - } 475 - } 476 - }; 477 - 478 - document.getElementById('configBlock').textContent = JSON.stringify(config, null, 2); 479 - } 480 - 481 466 function showNotification(message) { 482 467 const notification = document.getElementById('notification'); 483 468 notification.textContent = message; ··· 497 482 document.getElementById('apiKeyDisplay').style.display = 'block'; 498 483 document.getElementById('apiKeyValue').textContent = userData.api_key; 499 484 apiKeyVisible = true; 500 - updateMCPConfig(); 501 485 } else { 502 486 // Hide the key 503 487 document.getElementById('apiKeyHidden').style.display = 'block'; ··· 555 539 document.getElementById('apiKeyDisplay').style.display = 'block'; 556 540 document.getElementById('apiKeyValue').textContent = userData.api_key; 557 541 apiKeyVisible = true; 558 - updateMCPConfig(); 559 542 560 543 regenerateState = 'show-token'; 561 544 btn.disabled = false;
+10 -3
src/public/index.html
··· 228 228 .then(r => r.json()) 229 229 .then(data => { 230 230 const link = document.getElementById('git-hash-link'); 231 - link.href = `https://tangled.org/dunkirk.sh/canvas-mcp/commit/${data.hash}`; 232 - link.textContent = data.shortHash; 231 + if (link) { 232 + link.href = `https://tangled.org/dunkirk.sh/canvas-mcp/commit/${data.hash}`; 233 + link.textContent = data.shortHash; 234 + } 233 235 }) 234 236 .catch(() => { }); 235 237 ··· 237 239 fetch('/api/user/me', {credentials: 'include'}) 238 240 .then(r => { 239 241 if (r.ok) { 242 + console.log('[Index] User logged in, redirecting to dashboard'); 240 243 window.location.href = '/dashboard'; 244 + } else { 245 + console.log('[Index] User not logged in'); 241 246 } 242 247 }) 243 - .catch(() => { }); 248 + .catch(err => { 249 + console.log('[Index] Error checking auth:', err); 250 + }); 244 251 245 252 const form = document.getElementById('loginForm'); 246 253 const errorDiv = document.getElementById('error');