a fancy canvas mcp server!
1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <title>Canvas MCP Server</title>
8 <meta name="description"
9 content="Connect your Canvas LMS to AI assistants via the Model Context Protocol. Ask questions about your courses with Claude Desktop.">
10 <link rel="icon" type="image/x-icon" href="./favicon.ico">
11 <link rel="canonical" href="https://canvas.dunkirk.sh/" id="canonical-url">
12 <meta name="theme-color" content="#0066cc">
13
14 <!-- Open Graph / Facebook -->
15 <meta property="og:type" content="website">
16 <meta property="og:url" content="https://canvas.dunkirk.sh/" id="og-url">
17 <meta property="og:title" content="Canvas MCP Server">
18 <meta property="og:site_name" content="Canvas MCP Server">
19 <meta property="og:description"
20 content="Connect your Canvas LMS to AI assistants via the Model Context Protocol. Ask questions about your courses with Claude Desktop.">
21 <meta property="og:image" content="https://canvas.dunkirk.sh/og.png" id="og-image">
22 <meta property="og:image:width" content="1200">
23 <meta property="og:image:height" content="630">
24 <meta property="og:image:alt" content="Canvas MCP Server - Connect Canvas LMS to AI assistants">
25
26 <!-- Twitter -->
27 <meta property="twitter:card" content="summary_large_image">
28 <meta property="twitter:url" content="https://canvas.dunkirk.sh/" id="twitter-url">
29 <meta property="twitter:title" content="Canvas MCP Server">
30 <meta property="twitter:description"
31 content="Connect your Canvas LMS to AI assistants via the Model Context Protocol. Ask questions about your courses with Claude Desktop.">
32 <meta property="twitter:image" content="https://canvas.dunkirk.sh/og.png" id="twitter-image">
33
34 <script>
35 // Set dynamic URLs based on current host
36 const baseUrl = window.location.origin;
37 document.getElementById('canonical-url').setAttribute('href', `${baseUrl}/`);
38 document.getElementById('og-url').setAttribute('content', `${baseUrl}/`);
39 document.getElementById('og-image').setAttribute('content', `${baseUrl}/og.png`);
40 document.getElementById('twitter-url').setAttribute('content', `${baseUrl}/`);
41 document.getElementById('twitter-image').setAttribute('content', `${baseUrl}/og.png`);
42 </script>
43 <style>
44 * {
45 margin: 0;
46 padding: 0;
47 box-sizing: border-box;
48 }
49
50 body {
51 font-family: system-ui, -apple-system, sans-serif;
52 line-height: 1.6;
53 max-width: 600px;
54 margin: 4rem auto;
55 padding: 2rem;
56 color: #111;
57 }
58
59 h1 {
60 font-size: 2rem;
61 margin-bottom: 0.5rem;
62 font-weight: 600;
63 }
64
65 p {
66 color: #555;
67 margin-bottom: 2rem;
68 }
69
70 section {
71 margin: 2rem 0;
72 padding: 1.5rem;
73 border: 1px solid #ddd;
74 border-radius: 4px;
75 }
76
77 section h2 {
78 font-size: 1.1rem;
79 margin-bottom: 1rem;
80 font-weight: 600;
81 }
82
83 ul {
84 list-style: none;
85 padding-left: 1rem;
86 }
87
88 li {
89 padding: 0.25rem 0;
90 color: #555;
91 }
92
93 li::before {
94 content: "→ ";
95 margin-right: 0.5rem;
96 }
97
98 .login-form {
99 margin-top: 2rem;
100 }
101
102 label {
103 display: block;
104 margin-bottom: 0.5rem;
105 font-weight: 500;
106 color: #333;
107 }
108
109 input {
110 width: 100%;
111 padding: 0.75rem;
112 border: 1px solid #ddd;
113 border-radius: 4px;
114 font-size: 1rem;
115 font-family: inherit;
116 }
117
118 input:focus {
119 outline: none;
120 border-color: #0066cc;
121 }
122
123 button {
124 width: 100%;
125 margin-top: 1rem;
126 padding: 0.75rem;
127 background: #0066cc;
128 color: white;
129 border: none;
130 border-radius: 4px;
131 font-size: 1rem;
132 font-weight: 500;
133 cursor: pointer;
134 }
135
136 button:hover {
137 background: #0052a3;
138 }
139
140 button:disabled {
141 background: #ccc;
142 cursor: not-allowed;
143 }
144
145 .error {
146 margin-top: 1rem;
147 padding: 0.75rem;
148 background: #fee;
149 border: 1px solid #fcc;
150 border-radius: 4px;
151 color: #c33;
152 display: none;
153 }
154
155 .error.show {
156 display: block;
157 }
158
159 .success {
160 margin-top: 1rem;
161 padding: 0.75rem;
162 background: #d4edda;
163 border: 1px solid #c3e6cb;
164 border-radius: 4px;
165 color: #155724;
166 display: none;
167 }
168
169 .success.show {
170 display: block;
171 }
172
173 footer {
174 margin-top: 4rem;
175 padding-top: 2rem;
176 border-top: 1px solid #eee;
177 text-align: center;
178 color: #999;
179 font-size: 0.9rem;
180 }
181 </style>
182</head>
183
184<body>
185 <header>
186 <h1>Canvas MCP Server</h1>
187 <p>Connect your Canvas LMS to AI assistants via the Model Context Protocol</p>
188 </header>
189
190 <section>
191 <h2>How It Works</h2>
192 <ul>
193 <li>Sign in with your email (no password needed)</li>
194 <li>Connect your Canvas account with OAuth 2.1</li>
195 <li>Use your LLM choice ask questions about your courses</li>
196 <li>Works with any Canvas institution</li>
197 </ul>
198 </section>
199
200 <section class="login-form">
201 <h2>Get Started</h2>
202 <form id="loginForm">
203 <label for="email">Email Address</label>
204 <input type="email" id="email" name="email" placeholder="your.email@school.edu" autocomplete="email" required />
205
206 <button type="submit">Send Sign-In Link</button>
207 <div id="error" class="error"></div>
208 <div id="success" class="success"></div>
209 </form>
210
211 <p style="margin-top: 1.5rem; font-size: 0.9rem; color: #666;">
212 We'll send you a magic link to sign in. No password required.
213 </p>
214 </section>
215
216 <footer>
217 <div style="display: flex; justify-content: space-between; align-items: center;">
218 <span style="color: #666;">made by <a href="https://dunkirk.sh" style="color: #666; text-decoration: none;">kieran
219 klukas</a></span>
220 <a id="git-hash-link" href="#"
221 style="color: #999; text-decoration: none; font-family: monospace; font-size: 0.85rem;">...</a>
222 </div>
223 </footer>
224
225 <script type="module">
226 // Load git hash
227 fetch('/api/version')
228 .then(r => r.json())
229 .then(data => {
230 const link = document.getElementById('git-hash-link');
231 if (link) {
232 link.href = `https://tangled.org/dunkirk.sh/canvas-mcp/commit/${data.hash}`;
233 link.textContent = data.shortHash;
234 }
235 })
236 .catch(() => { });
237
238 // Check if already logged in
239 fetch('/api/user/me', {credentials: 'include'})
240 .then(r => {
241 if (r.ok) {
242 console.log('[Index] User logged in, redirecting to dashboard');
243 window.location.href = '/dashboard';
244 } else {
245 console.log('[Index] User not logged in');
246 }
247 })
248 .catch(err => {
249 console.log('[Index] Error checking auth:', err);
250 });
251
252 const form = document.getElementById('loginForm');
253 const errorDiv = document.getElementById('error');
254 const successDiv = document.getElementById('success');
255
256 function showError(message) {
257 errorDiv.textContent = message;
258 errorDiv.classList.add('show');
259 successDiv.classList.remove('show');
260 }
261
262 function showSuccess(message) {
263 successDiv.textContent = message;
264 successDiv.classList.add('show');
265 errorDiv.classList.remove('show');
266 }
267
268 function hideMessages() {
269 errorDiv.classList.remove('show');
270 successDiv.classList.remove('show');
271 }
272
273 form.addEventListener('submit', async (e) => {
274 e.preventDefault();
275 hideMessages();
276
277 const email = document.getElementById('email').value.trim();
278
279 if (!email) {
280 showError('Please enter your email address');
281 return;
282 }
283
284 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
285 showError('Please enter a valid email address');
286 return;
287 }
288
289 const submitBtn = form.querySelector('button');
290 submitBtn.disabled = true;
291 submitBtn.textContent = 'Sending...';
292
293 try {
294 const response = await fetch('/api/auth/request-magic-link', {
295 method: 'POST',
296 headers: {'Content-Type': 'application/json'},
297 body: JSON.stringify({email})
298 });
299
300 const data = await response.json();
301
302 if (!response.ok) {
303 throw new Error(data.error || 'Failed to send magic link');
304 }
305
306 showSuccess(`Check your email! We sent a sign-in link to ${email}`);
307 form.reset();
308 } catch (error) {
309 showError(error.message);
310 } finally {
311 submitBtn.disabled = false;
312 submitBtn.textContent = 'Send Sign-In Link';
313 }
314 });
315 </script>
316</body>
317
318</html>