a fancy canvas mcp server!
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}