a fancy canvas mcp server!
0
fork

Configure Feed

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

at main 260 lines 6.3 kB view raw
1import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2import { 3 CallToolRequestSchema, 4 ListToolsRequestSchema, 5 Tool, 6} from "@modelcontextprotocol/sdk/types.js"; 7import { z } from "zod"; 8import { CanvasClient } from "./canvas.js"; 9import DB from "./db.js"; 10 11// Create MCP Server instance with user context 12export function createMcpServer(userId: number): Server { 13 const BASE_URL = process.env.BASE_URL || "http://localhost:3000"; 14 15 const server = new Server( 16 { 17 name: "canvas-mcp", 18 version: "1.0.0", 19 title: "Canvas LMS", 20 description: 21 "Access your Canvas courses, assignments, grades, and announcements", 22 websiteUrl: BASE_URL, 23 icons: [ 24 { 25 src: `${BASE_URL}/favicon.ico`, 26 mimeType: "image/x-icon", 27 sizes: ["32x32"], 28 }, 29 ], 30 }, 31 { 32 capabilities: { 33 tools: {}, 34 }, 35 }, 36 ); 37 38 // Register handlers with user context 39 registerHandlers(server, userId); 40 41 return server; 42} 43 44function registerHandlers(mcpServer: Server, userId: number) { 45 // Define tool schemas 46 const listCoursesSchema = z.object({ 47 enrollment_state: z 48 .enum(["active", "completed", "invited", "rejected"]) 49 .optional(), 50 }); 51 52 const getAssignmentSchema = z.object({ 53 course_id: z.number(), 54 assignment_id: z.number(), 55 }); 56 57 const getAnnouncementsSchema = z.object({ 58 course_id: z.number().optional(), 59 limit: z.number().min(1).max(50).optional(), 60 }); 61 62 const getGradesSchema = z.object({ 63 course_id: z.number().optional(), 64 }); 65 66 // Tool definitions 67 const tools: Tool[] = [ 68 { 69 name: "list_courses", 70 description: 71 "List Canvas courses for the authenticated user. Can filter by enrollment state (active, completed, invited, rejected).", 72 inputSchema: { 73 type: "object", 74 properties: { 75 enrollment_state: { 76 type: "string", 77 enum: ["active", "completed", "invited", "rejected"], 78 description: "Filter courses by enrollment state", 79 }, 80 }, 81 }, 82 }, 83 { 84 name: "get_assignment", 85 description: 86 "Get detailed information about a specific assignment including description, due date, points, and submission details.", 87 inputSchema: { 88 type: "object", 89 properties: { 90 course_id: { 91 type: "number", 92 description: "The Canvas course ID", 93 }, 94 assignment_id: { 95 type: "number", 96 description: "The Canvas assignment ID", 97 }, 98 }, 99 required: ["course_id", "assignment_id"], 100 }, 101 }, 102 { 103 name: "get_upcoming_assignments", 104 description: 105 "Get upcoming assignments and deadlines for the next 30 days across all courses. Returns assignments with due dates, to-do items, and calendar events.", 106 inputSchema: { 107 type: "object", 108 properties: {}, 109 }, 110 }, 111 { 112 name: "get_announcements", 113 description: 114 "Get course announcements. Can retrieve announcements from a specific course or across all courses, sorted by most recent first.", 115 inputSchema: { 116 type: "object", 117 properties: { 118 course_id: { 119 type: "number", 120 description: 121 "Optional course ID to get announcements from a specific course. If not provided, returns announcements from all courses.", 122 }, 123 limit: { 124 type: "number", 125 description: 126 "Maximum number of announcements to return (1-50). Default is 10.", 127 }, 128 }, 129 }, 130 }, 131 { 132 name: "get_grades", 133 description: 134 "Get grades and submission information. Can retrieve grades for a specific course (including individual assignment submissions) or overall grades across all courses.", 135 inputSchema: { 136 type: "object", 137 properties: { 138 course_id: { 139 type: "number", 140 description: 141 "Optional course ID to get detailed grades and submissions for a specific course. If not provided, returns summary grades for all courses.", 142 }, 143 }, 144 }, 145 }, 146 ]; 147 148 // Register list_tools handler 149 mcpServer.setRequestHandler(ListToolsRequestSchema, async () => { 150 return { tools }; 151 }); 152 153 // Register call_tool handler 154 mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { 155 const { name, arguments: args } = request.params; 156 157 // Get user and Canvas token from database 158 const userData = DB.raw 159 .query("SELECT * FROM users WHERE id = ?") 160 .get(userId) as any; 161 162 if (!userData) { 163 throw new Error("User not found"); 164 } 165 166 const canvasToken = DB.getCanvasToken(userData); 167 const client = new CanvasClient(userData.canvas_domain, canvasToken); 168 169 // Log usage 170 DB.logUsage(userId, name); 171 DB.updateLastUsed(userId); 172 173 try { 174 switch (name) { 175 case "list_courses": { 176 const params = listCoursesSchema.parse(args); 177 const courses = await client.listCourses(params); 178 return { 179 content: [ 180 { 181 type: "text", 182 text: JSON.stringify(courses, null, 2), 183 }, 184 ], 185 }; 186 } 187 188 case "get_assignment": { 189 const params = getAssignmentSchema.parse(args); 190 const assignment = await client.getAssignment( 191 params.course_id, 192 params.assignment_id, 193 ); 194 return { 195 content: [ 196 { 197 type: "text", 198 text: JSON.stringify(assignment, null, 2), 199 }, 200 ], 201 }; 202 } 203 204 case "get_upcoming_assignments": { 205 const upcoming = await client.getUpcomingAssignments(); 206 return { 207 content: [ 208 { 209 type: "text", 210 text: JSON.stringify(upcoming, null, 2), 211 }, 212 ], 213 }; 214 } 215 216 case "get_announcements": { 217 const params = getAnnouncementsSchema.parse(args); 218 const announcements = await client.getCourseAnnouncements( 219 params.course_id, 220 params.limit, 221 ); 222 return { 223 content: [ 224 { 225 type: "text", 226 text: JSON.stringify(announcements, null, 2), 227 }, 228 ], 229 }; 230 } 231 232 case "get_grades": { 233 const params = getGradesSchema.parse(args); 234 const grades = await client.getGradesAndSubmissions(params.course_id); 235 return { 236 content: [ 237 { 238 type: "text", 239 text: JSON.stringify(grades, null, 2), 240 }, 241 ], 242 }; 243 } 244 245 default: 246 throw new Error(`Unknown tool: ${name}`); 247 } 248 } catch (error: any) { 249 return { 250 content: [ 251 { 252 type: "text", 253 text: `Error: ${error.message}`, 254 }, 255 ], 256 isError: true, 257 }; 258 } 259 }); 260}