Skip to main content
Glama
main.js11 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListResourcesRequestSchema, ReadResourceRequestSchema, SearchResourcesRequestSchema, WatchResourcesRequestSchema, ResourceUpdateMessage } from "@modelcontextprotocol/sdk/schema/index.js"; import axios from "axios"; class CanvasAPI { constructor(baseURL, accessToken) { this.api = axios.create({ baseURL, headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); } // Course Methods async listCourses(include = []) { try { const response = await this.api.get('/api/v1/courses', { params: { include: include.join(',') } }); return response.data; } catch (error) { throw this.handleError('Failed to list courses', error); } } async getCourse(courseId, include = []) { try { const response = await this.api.get(`/api/v1/courses/${courseId}`, { params: { include: include.join(',') } }); return response.data; } catch (error) { throw this.handleError(`Failed to get course ${courseId}`, error); } } // Module Methods async listModules(courseId, include = []) { try { const response = await this.api.get(`/api/v1/courses/${courseId}/modules`, { params: { include: include.join(',') } }); return response.data; } catch (error) { throw this.handleError(`Failed to list modules for course ${courseId}`, error); } } async getModuleItems(courseId, moduleId) { try { const response = await this.api.get(`/api/v1/courses/${courseId}/modules/${moduleId}/items`); return response.data; } catch (error) { throw this.handleError(`Failed to get items for module ${moduleId}`, error); } } // Assignment Methods async listAssignments(courseId, include = []) { try { const response = await this.api.get(`/api/v1/courses/${courseId}/assignments`, { params: { include: include.join(',') } }); return response.data; } catch (error) { throw this.handleError(`Failed to list assignments for course ${courseId}`, error); } } async getAssignment(courseId, assignmentId, include = []) { try { const response = await this.api.get(`/api/v1/courses/${courseId}/assignments/${assignmentId}`, { params: { include: include.join(',') } }); return response.data; } catch (error) { throw this.handleError(`Failed to get assignment ${assignmentId}`, error); } } // Submission Methods async listSubmissions(courseId, assignmentId, include = []) { try { const response = await this.api.get(`/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions`, { params: { include: include.join(',') } }); return response.data; } catch (error) { throw this.handleError(`Failed to list submissions for assignment ${assignmentId}`, error); } } // Grade Methods async getGrades(courseId, studentId) { try { const response = await this.api.get(`/api/v1/courses/${courseId}/students/${studentId}/grades`); return response.data; } catch (error) { throw this.handleError(`Failed to get grades for student ${studentId}`, error); } } handleError(message, error) { console.error(`Canvas API Error: ${message}`, error.response?.data || error); return new Error(`${message}: ${error.response?.data?.errors?.[0]?.message || error.message}`); } } class CanvasMCPServer { constructor(canvasURL, accessToken) { this.canvasAPI = new CanvasAPI(canvasURL, accessToken); this.watchedResources = new Map(); this.pollInterval = 30000; // 30 seconds this.server = new Server({ name: "canvas-lms-server", version: "1.0.0", }, { capabilities: { resources: { courses: true, modules: true, assignments: true, submissions: true, grades: true }, search: true, watch: true } }); this.setupRequestHandlers(); this.startWatchPolling(); } setupRequestHandlers() { // List available resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => { const courses = await this.canvasAPI.listCourses(['term', 'total_students']); return { resources: courses.map(course => ({ uri: `canvas:///courses/${course.id}`, name: course.name, metadata: { code: course.course_code, term: course.term?.name, startDate: course.start_at, endDate: course.end_at, students: course.total_students } })) }; }); // Read specific resource this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; const resourceType = this.parseResourceType(uri); const content = await this.fetchResourceContent(resourceType, uri); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(content, null, 2) }] }; }); // Search resources this.server.setRequestHandler(SearchResourcesRequestSchema, async (request) => { const query = request.params.query.toLowerCase(); const courses = await this.canvasAPI.listCourses(['term']); const matchingCourses = courses.filter(course => course.name.toLowerCase().includes(query) || course.course_code.toLowerCase().includes(query)); return { resources: matchingCourses.map(course => ({ uri: `canvas:///courses/${course.id}`, name: course.name, metadata: { code: course.course_code, term: course.term?.name } })) }; }); // Watch resources for changes this.server.setRequestHandler(WatchResourcesRequestSchema, async (request) => { const { resources } = request.params; for (const resource of resources) { if (!this.watchedResources.has(resource)) { this.watchedResources.set(resource, { lastCheck: new Date(), lastContent: await this.fetchResourceContent(this.parseResourceType(resource), resource) }); } } return { success: true }; }); } parseResourceType(uri) { const parts = uri.split('/'); return { type: parts[3], // courses, modules, assignments, etc. courseId: parts[4], subType: parts[5], // modules, assignments, submissions subId: parts[6] }; } async fetchResourceContent(resourceType, uri) { const { type, courseId, subType, subId } = resourceType; switch (type) { case 'courses': { const [course, modules, assignments] = await Promise.all([ this.canvasAPI.getCourse(courseId, ['term', 'teachers', 'total_students']), this.canvasAPI.listModules(courseId), this.canvasAPI.listAssignments(courseId, ['submission']) ]); return { course, modules, assignments }; } case 'modules': { if (subId) { return await this.canvasAPI.getModuleItems(courseId, subId); } return await this.canvasAPI.listModules(courseId); } case 'assignments': { if (subId) { if (subType === 'submissions') { return await this.canvasAPI.listSubmissions(courseId, subId, ['submission_comments']); } return await this.canvasAPI.getAssignment(courseId, subId, ['submission']); } return await this.canvasAPI.listAssignments(courseId, ['submission']); } default: throw new Error(`Unsupported resource type: ${type}`); } } async startWatchPolling() { setInterval(async () => { for (const [uri, data] of this.watchedResources.entries()) { try { const newContent = await this.fetchResourceContent(this.parseResourceType(uri), uri); if (JSON.stringify(newContent) !== JSON.stringify(data.lastContent)) { const message = new ResourceUpdateMessage({ uri, contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(newContent, null, 2) }] }); await this.server.broadcast(message); this.watchedResources.set(uri, { lastCheck: new Date(), lastContent: newContent }); } } catch (error) { console.error(`Error polling resource ${uri}:`, error); } } }, this.pollInterval); } async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.log('Canvas MCP Server started'); } } // Start the server const canvasURL = process.env.CANVAS_API_URL; const accessToken = process.env.CANVAS_ACCESS_TOKEN; if (!canvasURL || !accessToken) { console.error('Error: CANVAS_API_URL and CANVAS_ACCESS_TOKEN environment variables are required'); process.exit(1); } const server = new CanvasMCPServer(canvasURL, accessToken); await server.start();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DMontgomery40/mcp-canvas-lms'

If you have feedback or need assistance with the MCP directory API, please join our Discord server