<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas MCP Server</title>
<meta name="description"
content="Connect your Canvas LMS to AI assistants via the Model Context Protocol. Ask questions about your courses with Claude Desktop.">
<link rel="icon" type="image/x-icon" href="./favicon.ico">
<link rel="canonical" href="https://canvas.dunkirk.sh/" id="canonical-url">
<meta name="theme-color" content="#0066cc">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://canvas.dunkirk.sh/" id="og-url">
<meta property="og:title" content="Canvas MCP Server">
<meta property="og:site_name" content="Canvas MCP Server">
<meta property="og:description"
content="Connect your Canvas LMS to AI assistants via the Model Context Protocol. Ask questions about your courses with Claude Desktop.">
<meta property="og:image" content="https://canvas.dunkirk.sh/og.png" id="og-image">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Canvas MCP Server - Connect Canvas LMS to AI assistants">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://canvas.dunkirk.sh/" id="twitter-url">
<meta property="twitter:title" content="Canvas MCP Server">
<meta property="twitter:description"
content="Connect your Canvas LMS to AI assistants via the Model Context Protocol. Ask questions about your courses with Claude Desktop.">
<meta property="twitter:image" content="https://canvas.dunkirk.sh/og.png" id="twitter-image">
<script>
// Set dynamic URLs based on current host
const baseUrl = window.location.origin;
document.getElementById('canonical-url').setAttribute('href', `${baseUrl}/`);
document.getElementById('og-url').setAttribute('content', `${baseUrl}/`);
document.getElementById('og-image').setAttribute('content', `${baseUrl}/og.png`);
document.getElementById('twitter-url').setAttribute('content', `${baseUrl}/`);
document.getElementById('twitter-image').setAttribute('content', `${baseUrl}/og.png`);
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
max-width: 600px;
margin: 4rem auto;
padding: 2rem;
color: #111;
}
h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
font-weight: 600;
}
p {
color: #555;
margin-bottom: 2rem;
}
section {
margin: 2rem 0;
padding: 1.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
section h2 {
font-size: 1.1rem;
margin-bottom: 1rem;
font-weight: 600;
}
ul {
list-style: none;
padding-left: 1rem;
}
li {
padding: 0.25rem 0;
color: #555;
}
li::before {
content: "→ ";
margin-right: 0.5rem;
}
.login-form {
margin-top: 2rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
font-family: inherit;
}
input:focus {
outline: none;
border-color: #0066cc;
}
button {
width: 100%;
margin-top: 1rem;
padding: 0.75rem;
background: #0066cc;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
}
button:hover {
background: #0052a3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.error {
margin-top: 1rem;
padding: 0.75rem;
background: #fee;
border: 1px solid #fcc;
border-radius: 4px;
color: #c33;
display: none;
}
.error.show {
display: block;
}
.success {
margin-top: 1rem;
padding: 0.75rem;
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 4px;
color: #155724;
display: none;
}
.success.show {
display: block;
}
footer {
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid #eee;
text-align: center;
color: #999;
font-size: 0.9rem;
}
</style>
</head>
<body>
<header>
<h1>Canvas MCP Server</h1>
<p>Connect your Canvas LMS to AI assistants via the Model Context Protocol</p>
</header>
<section>
<h2>How It Works</h2>
<ul>
<li>Sign in with your email (no password needed)</li>
<li>Connect your Canvas account with OAuth 2.1</li>
<li>Use your LLM choice ask questions about your courses</li>
<li>Works with any Canvas institution</li>
</ul>
</section>
<section class="login-form">
<h2>Get Started</h2>
<form id="loginForm">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" placeholder="your.email@school.edu" autocomplete="email" required />
<button type="submit">Send Sign-In Link</button>
<div id="error" class="error"></div>
<div id="success" class="success"></div>
</form>
<p style="margin-top: 1.5rem; font-size: 0.9rem; color: #666;">
We'll send you a magic link to sign in. No password required.
</p>
</section>
<footer>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="color: #666;">made by <a href="https://dunkirk.sh" style="color: #666; text-decoration: none;">kieran
klukas</a></span>
<a id="git-hash-link" href="#"
style="color: #999; text-decoration: none; font-family: monospace; font-size: 0.85rem;">...</a>
</div>
</footer>
<script type="module">
// Load git hash
fetch('/api/version')
.then(r => r.json())
.then(data => {
const link = document.getElementById('git-hash-link');
if (link) {
link.href = `https://tangled.org/dunkirk.sh/canvas-mcp/commit/${data.hash}`;
link.textContent = data.shortHash;
}
})
.catch(() => { });
// Check if already logged in
fetch('/api/user/me', {credentials: 'include'})
.then(r => {
if (r.ok) {
console.log('[Index] User logged in, redirecting to dashboard');
window.location.href = '/dashboard';
} else {
console.log('[Index] User not logged in');
}
})
.catch(err => {
console.log('[Index] Error checking auth:', err);
});
const form = document.getElementById('loginForm');
const errorDiv = document.getElementById('error');
const successDiv = document.getElementById('success');
function showError(message) {
errorDiv.textContent = message;
errorDiv.classList.add('show');
successDiv.classList.remove('show');
}
function showSuccess(message) {
successDiv.textContent = message;
successDiv.classList.add('show');
errorDiv.classList.remove('show');
}
function hideMessages() {
errorDiv.classList.remove('show');
successDiv.classList.remove('show');
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
hideMessages();
const email = document.getElementById('email').value.trim();
if (!email) {
showError('Please enter your email address');
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showError('Please enter a valid email address');
return;
}
const submitBtn = form.querySelector('button');
submitBtn.disabled = true;
submitBtn.textContent = 'Sending...';
try {
const response = await fetch('/api/auth/request-magic-link', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to send magic link');
}
showSuccess(`Check your email! We sent a sign-in link to ${email}`);
form.reset();
} catch (error) {
showError(error.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Send Sign-In Link';
}
});
</script>
</body>
</html>