@charlie.act7/canvas-mcp-server 1.1.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,17 @@
1
1
  import axios from 'axios';
2
2
  export class CanvasClient {
3
3
  client;
4
+ quizClient;
4
5
  token;
5
6
  domain;
6
7
  constructor(token, domain) {
7
8
  this.token = token;
8
9
  this.domain = domain;
9
10
  this.client = this.createAxiosInstance(token, domain);
11
+ this.quizClient = this.createAxiosInstance(token, domain, 'api/quiz/v1');
10
12
  }
11
- createAxiosInstance(token, domain) {
12
- const baseURL = `https://${domain}/api/v1`;
13
+ createAxiosInstance(token, domain, apiPath = 'api/v1') {
14
+ const baseURL = `https://${domain}/${apiPath}`;
13
15
  return axios.create({
14
16
  baseURL,
15
17
  headers: {
@@ -22,6 +24,7 @@ export class CanvasClient {
22
24
  this.token = token;
23
25
  this.domain = domain;
24
26
  this.client = this.createAxiosInstance(token, domain);
27
+ this.quizClient = this.createAxiosInstance(token, domain, 'api/quiz/v1');
25
28
  }
26
29
  parseLinkHeader(header) {
27
30
  if (!header)
@@ -189,9 +192,6 @@ export class CanvasClient {
189
192
  await this.client.delete(url);
190
193
  return { deleted: true, comment_id: commentId };
191
194
  }
192
- async getSubmissionComments(courseId, assignmentId, userId) {
193
- return this.getSingleSubmission(courseId, assignmentId, userId);
194
- }
195
195
  async getDiscussionTopics(courseId) {
196
196
  return this.getAllPages(`courses/${courseId}/discussion_topics`);
197
197
  }
@@ -213,6 +213,11 @@ export class CanvasClient {
213
213
  });
214
214
  return response.data;
215
215
  }
216
+ async updateAnnouncement(courseId, topicId, fields) {
217
+ const url = `courses/${courseId}/discussion_topics/${topicId}`;
218
+ const response = await this.client.put(url, fields);
219
+ return response.data;
220
+ }
216
221
  // --- Quiz Questions ---
217
222
  async listQuizQuestions(courseId, quizId) {
218
223
  return this.getAllPages(`courses/${courseId}/quizzes/${quizId}/questions`, {
@@ -378,6 +383,15 @@ export class CanvasClient {
378
383
  return { deleted: true };
379
384
  }
380
385
  // --- Rubrics ---
386
+ async getRubrics(courseId) {
387
+ return this.getAllPages(`courses/${courseId}/rubrics`);
388
+ }
389
+ async getRubric(courseId, rubricId, include) {
390
+ const response = await this.client.get(`courses/${courseId}/rubrics/${rubricId}`, {
391
+ params: { include }
392
+ });
393
+ return response.data;
394
+ }
381
395
  async createRubric(courseId, rubricData, rubricAssociationData) {
382
396
  const payload = { rubric: rubricData };
383
397
  if (rubricAssociationData) {
@@ -386,6 +400,14 @@ export class CanvasClient {
386
400
  const response = await this.client.post(`courses/${courseId}/rubrics`, payload);
387
401
  return response.data;
388
402
  }
403
+ async updateRubric(courseId, rubricId, rubricData, rubricAssociationData) {
404
+ const payload = { rubric: rubricData };
405
+ if (rubricAssociationData) {
406
+ payload.rubric_association = rubricAssociationData;
407
+ }
408
+ const response = await this.client.put(`courses/${courseId}/rubrics/${rubricId}`, payload);
409
+ return response.data;
410
+ }
389
411
  async createRubricAssociation(courseId, associationData) {
390
412
  const response = await this.client.post(`courses/${courseId}/rubric_associations`, {
391
413
  rubric_association: associationData
@@ -456,4 +478,225 @@ export class CanvasClient {
456
478
  const response = await this.client.post(`groups/${groupId}/memberships`, { user_id: userId });
457
479
  return response.data;
458
480
  }
481
+ // --- Course Management ---
482
+ async createCourse(accountId, data) {
483
+ const response = await this.client.post(`accounts/${accountId}/courses`, { course: data });
484
+ return response.data;
485
+ }
486
+ async updateCourse(courseId, data) {
487
+ const response = await this.client.put(`courses/${courseId}`, { course: data });
488
+ return response.data;
489
+ }
490
+ async getSyllabus(courseId) {
491
+ const response = await this.client.get(`courses/${courseId}`, { params: { include: ['syllabus_body'] } });
492
+ return response.data;
493
+ }
494
+ async healthCheck() {
495
+ const response = await this.client.get('users/self/profile');
496
+ return { status: 'ok', user: response.data.name, domain: this.domain };
497
+ }
498
+ // --- Assignment ---
499
+ async deleteAssignment(courseId, assignmentId) {
500
+ await this.client.delete(`courses/${courseId}/assignments/${assignmentId}`);
501
+ return { deleted: true };
502
+ }
503
+ // --- Pages ---
504
+ async deletePage(courseId, pageUrlOrId) {
505
+ await this.client.delete(`courses/${courseId}/pages/${pageUrlOrId}`);
506
+ return { deleted: true };
507
+ }
508
+ // --- Discussions ---
509
+ async createDiscussion(courseId, data) {
510
+ const response = await this.client.post(`courses/${courseId}/discussion_topics`, data);
511
+ return response.data;
512
+ }
513
+ async deleteDiscussion(courseId, topicId) {
514
+ await this.client.delete(`courses/${courseId}/discussion_topics/${topicId}`);
515
+ return { deleted: true };
516
+ }
517
+ // --- Users & Enrollments ---
518
+ async getProfile() {
519
+ const response = await this.client.get('users/self/profile');
520
+ return response.data;
521
+ }
522
+ async getUser(userId) {
523
+ const response = await this.client.get(`users/${userId}`);
524
+ return response.data;
525
+ }
526
+ async searchUsers(accountId, searchTerm, enrollmentType) {
527
+ return this.getAllPages(`accounts/${accountId}/users`, {
528
+ search_term: searchTerm,
529
+ enrollment_type: enrollmentType,
530
+ per_page: 50
531
+ });
532
+ }
533
+ async listCourseEnrollments(courseId, type) {
534
+ return this.getAllPages(`courses/${courseId}/enrollments`, {
535
+ type,
536
+ include: ['user'],
537
+ per_page: 100
538
+ });
539
+ }
540
+ async enrollUser(courseId, userId, enrollmentType, notify) {
541
+ const response = await this.client.post(`courses/${courseId}/enrollments`, {
542
+ enrollment: {
543
+ user_id: userId,
544
+ type: enrollmentType,
545
+ enrollment_state: 'active',
546
+ notify: notify ?? false
547
+ }
548
+ });
549
+ return response.data;
550
+ }
551
+ async removeEnrollment(courseId, enrollmentId, task = 'conclude') {
552
+ const response = await this.client.delete(`courses/${courseId}/enrollments/${enrollmentId}`, { params: { task } });
553
+ return response.data;
554
+ }
555
+ // --- Student Submissions ---
556
+ async submitAssignmentFile(courseId, assignmentId, filePath, fileName, contentType) {
557
+ // Step 1: Pre-flight to get submission upload URL
558
+ const preflightResponse = await this.client.post(`courses/${courseId}/assignments/${assignmentId}/submissions/self/files`, { name: fileName, content_type: contentType });
559
+ const { upload_url, upload_params } = preflightResponse.data;
560
+ // Step 2: Upload file to storage
561
+ const fs = await import('node:fs');
562
+ const FormData = (await import('form-data')).default;
563
+ const form = new FormData();
564
+ Object.entries(upload_params).forEach(([key, value]) => {
565
+ form.append(key, value);
566
+ });
567
+ form.append('file', fs.createReadStream(filePath));
568
+ const uploadResponse = await axios.post(upload_url, form, {
569
+ headers: form.getHeaders(),
570
+ maxRedirects: 0,
571
+ validateStatus: (s) => s < 400
572
+ });
573
+ // Step 3: Confirm upload and get file ID
574
+ let fileId;
575
+ const location = uploadResponse.headers['location'] || uploadResponse.data?.location;
576
+ if (location) {
577
+ const confirmResponse = await this.client.get(location);
578
+ fileId = confirmResponse.data.id;
579
+ }
580
+ else {
581
+ fileId = uploadResponse.data.id;
582
+ }
583
+ // Step 4: Submit the assignment with the uploaded file
584
+ const submissionResponse = await this.client.post(`courses/${courseId}/assignments/${assignmentId}/submissions`, {
585
+ submission: {
586
+ submission_type: 'online_upload',
587
+ file_ids: [fileId]
588
+ }
589
+ });
590
+ return submissionResponse.data;
591
+ }
592
+ // --- Conversations ---
593
+ async listConversations(scope, perPage = 50) {
594
+ return this.getAllPages('conversations', {
595
+ scope: scope ?? 'inbox',
596
+ per_page: perPage
597
+ });
598
+ }
599
+ async getConversation(conversationId) {
600
+ const response = await this.client.get(`conversations/${conversationId}`);
601
+ return response.data;
602
+ }
603
+ async getConversationUnreadCount() {
604
+ const response = await this.client.get('conversations/unread_count');
605
+ return response.data;
606
+ }
607
+ async sendConversation(data) {
608
+ const payload = {
609
+ recipients: data.recipients,
610
+ subject: data.subject,
611
+ body: data.body,
612
+ group_conversation: data.group_conversation ?? false,
613
+ bulk_message: data.bulk_message ?? false
614
+ };
615
+ if (data.course_id) {
616
+ payload.context_code = `course_${data.course_id}`;
617
+ }
618
+ const response = await this.client.post('conversations', payload);
619
+ return response.data;
620
+ }
621
+ async replyToConversation(conversationId, body) {
622
+ const response = await this.client.post(`conversations/${conversationId}/add_message`, { body });
623
+ return response.data;
624
+ }
625
+ // --- Analytics ---
626
+ async getCourseAnalytics(courseId) {
627
+ const response = await this.client.get(`courses/${courseId}/analytics/activity`);
628
+ return response.data;
629
+ }
630
+ async getStudentAnalytics(courseId, studentId) {
631
+ const response = await this.client.get(`courses/${courseId}/analytics/users/${studentId}/activity`);
632
+ return response.data;
633
+ }
634
+ async getCourseActivityStream(courseId) {
635
+ const response = await this.client.get(`courses/${courseId}/activity_stream/summary`);
636
+ return response.data;
637
+ }
638
+ async searchCourseContent(courseId, query, contentTypes) {
639
+ const types = contentTypes && contentTypes.length > 0 ? contentTypes : ['assignments', 'pages', 'discussion_topics', 'quizzes', 'files'];
640
+ const results = [];
641
+ for (const type of types) {
642
+ try {
643
+ const items = await this.getAllPages(`courses/${courseId}/${type}`, { per_page: 50 });
644
+ const q = query.toLowerCase();
645
+ const matched = items.filter((i) => (i.name || i.title || '').toLowerCase().includes(q)).map((i) => ({ ...i, _content_type: type }));
646
+ results.push(...matched);
647
+ }
648
+ catch {
649
+ // skip types the user doesn't have access to
650
+ }
651
+ }
652
+ return results;
653
+ }
654
+ // --- Peer Reviews ---
655
+ async listPeerReviews(courseId, assignmentId) {
656
+ return this.getAllPages(`courses/${courseId}/assignments/${assignmentId}/peer_reviews`, { include: ['submission_comments', 'user'], per_page: 100 });
657
+ }
658
+ async getSubmissionPeerReviews(courseId, assignmentId, submissionId) {
659
+ return this.getAllPages(`courses/${courseId}/assignments/${assignmentId}/submissions/${submissionId}/peer_reviews`, { include: ['submission_comments', 'user'], per_page: 100 });
660
+ }
661
+ async createPeerReview(courseId, assignmentId, submissionId, revieweeId) {
662
+ const response = await this.client.post(`courses/${courseId}/assignments/${assignmentId}/submissions/${submissionId}/peer_reviews`, { user_id: revieweeId });
663
+ return response.data;
664
+ }
665
+ async deletePeerReview(courseId, assignmentId, submissionId, revieweeId) {
666
+ await this.client.delete(`courses/${courseId}/assignments/${assignmentId}/submissions/${submissionId}/peer_reviews`, { params: { user_id: revieweeId } });
667
+ return { deleted: true };
668
+ }
669
+ // --- New Quizzes (LTI) ---
670
+ async createNewQuiz(courseId, data) {
671
+ const response = await this.quizClient.post(`courses/${courseId}/quizzes`, data);
672
+ return response.data;
673
+ }
674
+ async updateNewQuiz(courseId, quizId, data) {
675
+ const response = await this.quizClient.patch(`courses/${courseId}/quizzes/${quizId}`, data);
676
+ return response.data;
677
+ }
678
+ async deleteNewQuiz(courseId, quizId) {
679
+ await this.quizClient.delete(`courses/${courseId}/quizzes/${quizId}`);
680
+ return { deleted: true };
681
+ }
682
+ async listNewQuizItems(courseId, quizId) {
683
+ const response = await this.quizClient.get(`courses/${courseId}/quizzes/${quizId}/items`);
684
+ return response.data.items ?? response.data ?? [];
685
+ }
686
+ async getNewQuizItem(courseId, quizId, itemId) {
687
+ const response = await this.quizClient.get(`courses/${courseId}/quizzes/${quizId}/items/${itemId}`);
688
+ return response.data;
689
+ }
690
+ async createNewQuizItem(courseId, quizId, data) {
691
+ const response = await this.quizClient.post(`courses/${courseId}/quizzes/${quizId}/items`, data);
692
+ return response.data;
693
+ }
694
+ async updateNewQuizItem(courseId, quizId, itemId, data) {
695
+ const response = await this.quizClient.patch(`courses/${courseId}/quizzes/${quizId}/items/${itemId}`, data);
696
+ return response.data;
697
+ }
698
+ async deleteNewQuizItem(courseId, quizId, itemId) {
699
+ await this.quizClient.delete(`courses/${courseId}/quizzes/${quizId}/items/${itemId}`);
700
+ return { deleted: true };
701
+ }
459
702
  }
@@ -0,0 +1,124 @@
1
+ import { GoogleGenAI } from "@google/genai";
2
+ const DEFAULT_MODEL = "gemini-2.5-flash";
3
+ // Gemini no acepta: $schema, additionalProperties, anyOf multi-tipo, nullable
4
+ function sanitizeSchema(schema) {
5
+ const { $schema, additionalProperties, nullable, ...clean } = schema;
6
+ // anyOf con un solo elemento → aplanar
7
+ if (Array.isArray(clean.anyOf) && clean.anyOf.length === 1) {
8
+ return sanitizeSchema(clean.anyOf[0]);
9
+ }
10
+ // anyOf con múltiples tipos (ej: number | string | null) → convertir a string
11
+ if (Array.isArray(clean.anyOf)) {
12
+ return { type: "string", description: clean.description ?? "" };
13
+ }
14
+ // type: ["string", "null"] → type: "string"
15
+ if (Array.isArray(clean.type)) {
16
+ clean.type = clean.type.find((t) => t !== "null") ?? "string";
17
+ }
18
+ if (clean.properties) {
19
+ const cleanProps = {};
20
+ for (const [k, v] of Object.entries(clean.properties)) {
21
+ cleanProps[k] = sanitizeSchema(v);
22
+ }
23
+ clean.properties = cleanProps;
24
+ }
25
+ if (clean.items) {
26
+ clean.items = sanitizeSchema(clean.items);
27
+ }
28
+ return clean;
29
+ }
30
+ function toolsToGemini(tools) {
31
+ const seen = new Set();
32
+ const unique = tools.filter(t => {
33
+ if (seen.has(t.tool.name))
34
+ return false;
35
+ seen.add(t.tool.name);
36
+ return true;
37
+ });
38
+ return [{
39
+ functionDeclarations: unique.map(t => ({
40
+ name: t.tool.name,
41
+ description: t.tool.description ?? "",
42
+ parameters: sanitizeSchema(t.tool.inputSchema)
43
+ }))
44
+ }];
45
+ }
46
+ export class GeminiRunner {
47
+ ai;
48
+ tools;
49
+ client;
50
+ constructor(apiKey, client, tools) {
51
+ this.ai = new GoogleGenAI({ apiKey });
52
+ this.client = client;
53
+ this.tools = tools;
54
+ }
55
+ async run(userMessage, options = {}) {
56
+ const model = options.model ?? DEFAULT_MODEL;
57
+ const geminiTools = toolsToGemini(this.tools);
58
+ const systemInstruction = options.systemPrompt ??
59
+ "Eres un asistente educativo con acceso a Canvas LMS. " +
60
+ "Usa las herramientas disponibles para responder sobre cursos, tareas, estudiantes y calificaciones. " +
61
+ "Responde SIEMPRE en español. Sé conciso y claro.";
62
+ const toolsUsed = [];
63
+ // Historial de conversación en formato Gemini
64
+ const contents = [
65
+ { role: "user", parts: [{ text: userMessage }] }
66
+ ];
67
+ // Agentic loop (máx. 10 iteraciones)
68
+ for (let i = 0; i < 10; i++) {
69
+ const response = await this.ai.models.generateContent({
70
+ model,
71
+ contents,
72
+ config: {
73
+ tools: geminiTools,
74
+ systemInstruction,
75
+ thinkingConfig: { thinkingBudget: 0 } // deshabilitar thinking para evitar respuestas vacías
76
+ }
77
+ });
78
+ const candidate = response.candidates?.[0];
79
+ if (!candidate)
80
+ throw new Error("Gemini no devolvió ningún candidato.");
81
+ const parts = candidate.content?.parts ?? [];
82
+ // Agregar respuesta del modelo al historial
83
+ contents.push({ role: "model", parts });
84
+ // Separar texto y function calls
85
+ const functionCalls = parts.filter((p) => p.functionCall);
86
+ const textParts = parts.filter((p) => p.text);
87
+ // Sin function calls → respuesta final
88
+ if (functionCalls.length === 0) {
89
+ const answer = textParts.map((p) => p.text).join("").trim();
90
+ return { answer, tools_used: toolsUsed, model, provider: "gemini" };
91
+ }
92
+ // Ejecutar cada function call
93
+ const functionResponses = [];
94
+ for (const part of functionCalls) {
95
+ const { name, args } = part.functionCall;
96
+ const toolDef = this.tools.find(t => t.name === name);
97
+ let resultText;
98
+ if (!toolDef) {
99
+ resultText = `Error: herramienta '${name}' no existe.`;
100
+ }
101
+ else {
102
+ try {
103
+ const result = await toolDef.handler(this.client, args ?? {});
104
+ const blocks = result?.content ?? [];
105
+ resultText = blocks
106
+ .filter(b => b.type === "text" && b.text)
107
+ .map(b => b.text)
108
+ .join("\n");
109
+ toolsUsed.push(name);
110
+ }
111
+ catch (err) {
112
+ resultText = `Error: ${err instanceof Error ? err.message : String(err)}`;
113
+ }
114
+ }
115
+ functionResponses.push({
116
+ functionResponse: { name, response: { result: resultText } }
117
+ });
118
+ }
119
+ // Devolver resultados al modelo
120
+ contents.push({ role: "user", parts: functionResponses });
121
+ }
122
+ throw new Error("El agente excedió el límite de iteraciones (10).");
123
+ }
124
+ }
@@ -0,0 +1,118 @@
1
+ import { resolveCourseId } from "../common/helpers.js";
2
+ import { z } from "zod";
3
+ export const analyticsTools = [
4
+ {
5
+ name: "canvas_get_course_analytics",
6
+ tool: {
7
+ name: "canvas_get_course_analytics",
8
+ description: "Get participation and activity analytics for a course (page views, participations per day)",
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: {
12
+ course_id: {
13
+ anyOf: [{ type: "number" }, { type: "string" }],
14
+ description: "The ID or name of the course"
15
+ }
16
+ },
17
+ required: ["course_id"],
18
+ },
19
+ },
20
+ handler: async (client, args) => {
21
+ const input = z.object({ course_id: z.union([z.number(), z.string()]) }).parse(args);
22
+ const courseId = await resolveCourseId(client, input.course_id);
23
+ const result = await client.getCourseAnalytics(courseId);
24
+ return {
25
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
26
+ };
27
+ }
28
+ },
29
+ {
30
+ name: "canvas_get_student_analytics",
31
+ tool: {
32
+ name: "canvas_get_student_analytics",
33
+ description: "Get activity analytics for a specific student in a course (page views, participations, tardiness breakdown)",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ course_id: {
38
+ anyOf: [{ type: "number" }, { type: "string" }],
39
+ description: "The ID or name of the course"
40
+ },
41
+ student_id: { type: "number", description: "The Canvas user ID of the student" }
42
+ },
43
+ required: ["course_id", "student_id"],
44
+ },
45
+ },
46
+ handler: async (client, args) => {
47
+ const input = z.object({
48
+ course_id: z.union([z.number(), z.string()]),
49
+ student_id: z.coerce.number()
50
+ }).parse(args);
51
+ const courseId = await resolveCourseId(client, input.course_id);
52
+ const result = await client.getStudentAnalytics(courseId, input.student_id);
53
+ return {
54
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
55
+ };
56
+ }
57
+ },
58
+ {
59
+ name: "canvas_get_course_activity_stream",
60
+ tool: {
61
+ name: "canvas_get_course_activity_stream",
62
+ description: "Get a summary of recent activity in a course (submissions, discussions, announcements, etc.)",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ course_id: {
67
+ anyOf: [{ type: "number" }, { type: "string" }],
68
+ description: "The ID or name of the course"
69
+ }
70
+ },
71
+ required: ["course_id"],
72
+ },
73
+ },
74
+ handler: async (client, args) => {
75
+ const input = z.object({ course_id: z.union([z.number(), z.string()]) }).parse(args);
76
+ const courseId = await resolveCourseId(client, input.course_id);
77
+ const result = await client.getCourseActivityStream(courseId);
78
+ return {
79
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
80
+ };
81
+ }
82
+ },
83
+ {
84
+ name: "canvas_search_course_content",
85
+ tool: {
86
+ name: "canvas_search_course_content",
87
+ description: "Search for content across a course by keyword (assignments, pages, discussions, quizzes, files)",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {
91
+ course_id: {
92
+ anyOf: [{ type: "number" }, { type: "string" }],
93
+ description: "The ID or name of the course"
94
+ },
95
+ query: { type: "string", description: "Search keyword" },
96
+ content_types: {
97
+ type: "array",
98
+ items: { type: "string" },
99
+ description: "Limit search to specific types: assignments, pages, discussion_topics, quizzes, files. Defaults to all."
100
+ }
101
+ },
102
+ required: ["course_id", "query"],
103
+ },
104
+ },
105
+ handler: async (client, args) => {
106
+ const input = z.object({
107
+ course_id: z.union([z.number(), z.string()]),
108
+ query: z.string().min(1),
109
+ content_types: z.array(z.string()).optional()
110
+ }).parse(args);
111
+ const courseId = await resolveCourseId(client, input.course_id);
112
+ const result = await client.searchCourseContent(courseId, input.query, input.content_types);
113
+ return {
114
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
115
+ };
116
+ }
117
+ }
118
+ ];
@@ -214,7 +214,7 @@ export const assignmentTools = [
214
214
  student_id: z.coerce.number()
215
215
  }).parse(args);
216
216
  const courseId = await resolveCourseId(client, input.course_id);
217
- const submission = await client.getSubmissionComments(courseId, input.assignment_id, input.student_id);
217
+ const submission = await client.getSingleSubmission(courseId, input.assignment_id, input.student_id);
218
218
  return {
219
219
  content: [{ type: "text", text: JSON.stringify(submission.submission_comments || [], null, 2) }],
220
220
  };
@@ -518,6 +518,82 @@ export const assignmentTools = [
518
518
  };
519
519
  }
520
520
  },
521
+ {
522
+ name: "canvas_submit_assignment",
523
+ tool: {
524
+ name: "canvas_submit_assignment",
525
+ description: "Submit a file upload assignment as a student. Uploads a local file and creates the submission in one step.",
526
+ inputSchema: {
527
+ type: "object",
528
+ properties: {
529
+ course_id: {
530
+ anyOf: [{ type: "number" }, { type: "string" }],
531
+ description: "The ID or name of the course"
532
+ },
533
+ assignment_id: {
534
+ type: "number",
535
+ description: "The ID of the assignment to submit"
536
+ },
537
+ file_path: {
538
+ type: "string",
539
+ description: "Absolute local path to the file to submit (e.g. C:/Users/name/file.pdf)"
540
+ },
541
+ file_name: {
542
+ type: "string",
543
+ description: "File name to use in Canvas (e.g. MySubmission.pdf)"
544
+ },
545
+ content_type: {
546
+ type: "string",
547
+ description: "MIME type of the file (e.g. application/pdf)"
548
+ }
549
+ },
550
+ required: ["course_id", "assignment_id", "file_path", "file_name", "content_type"]
551
+ }
552
+ },
553
+ handler: async (client, args) => {
554
+ const input = z.object({
555
+ course_id: z.union([z.number(), z.string()]),
556
+ assignment_id: z.coerce.number(),
557
+ file_path: z.string(),
558
+ file_name: z.string(),
559
+ content_type: z.string()
560
+ }).parse(args);
561
+ const courseId = await resolveCourseId(client, input.course_id);
562
+ const submission = await client.submitAssignmentFile(courseId, input.assignment_id, input.file_path, input.file_name, input.content_type);
563
+ return {
564
+ content: [{ type: "text", text: JSON.stringify(submission, null, 2) }]
565
+ };
566
+ }
567
+ },
568
+ {
569
+ name: "canvas_delete_assignment",
570
+ tool: {
571
+ name: "canvas_delete_assignment",
572
+ description: "Delete an assignment from a course",
573
+ inputSchema: {
574
+ type: "object",
575
+ properties: {
576
+ course_id: {
577
+ anyOf: [{ type: "number" }, { type: "string" }],
578
+ description: "The ID or name of the course"
579
+ },
580
+ assignment_id: { type: "number", description: "The ID of the assignment to delete" }
581
+ },
582
+ required: ["course_id", "assignment_id"],
583
+ },
584
+ },
585
+ handler: async (client, args) => {
586
+ const input = z.object({
587
+ course_id: z.union([z.number(), z.string()]),
588
+ assignment_id: z.coerce.number()
589
+ }).parse(args);
590
+ const courseId = await resolveCourseId(client, input.course_id);
591
+ const result = await client.deleteAssignment(courseId, input.assignment_id);
592
+ return {
593
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
594
+ };
595
+ }
596
+ },
521
597
  {
522
598
  name: "canvas_update_assignment",
523
599
  tool: {