@charlie.act7/canvas-mcp-server 1.1.3 → 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.
package/dist/index.js CHANGED
@@ -23,6 +23,11 @@ import { configTools } from "./tools/config-tools.js";
23
23
  import { rubricTools } from "./tools/rubric-tools.js";
24
24
  import { calendarTools } from "./tools/calendar-tools.js";
25
25
  import { groupTools } from "./tools/group-tools.js";
26
+ import { enrollmentTools } from "./tools/enrollment-tools.js";
27
+ import { conversationTools } from "./tools/conversation-tools.js";
28
+ import { newQuizTools } from "./tools/new-quiz-tools.js";
29
+ import { analyticsTools } from "./tools/analytics-tools.js";
30
+ import { peerReviewTools } from "./tools/peer-review-tools.js";
26
31
  import { canvasResources } from "./resources/canvas-resources.js";
27
32
  import { canvasPrompts } from "./prompts/canvas-prompts.js";
28
33
  import { startHttpServer } from "./http-server.js";
@@ -33,7 +38,7 @@ const program = new Command();
33
38
  program
34
39
  .name("canvas-mcp")
35
40
  .description("MCP Server for Canvas LMS (Refactored)")
36
- .version("1.1.0");
41
+ .version("1.2.0");
37
42
  // --- CLI Commands ---
38
43
  program
39
44
  .command("config")
@@ -82,7 +87,7 @@ program
82
87
  const client = getClient();
83
88
  const server = new Server({
84
89
  name: "canvas-lms-server",
85
- version: "1.0.0",
90
+ version: "1.2.0",
86
91
  }, {
87
92
  capabilities: {
88
93
  tools: {},
@@ -105,7 +110,12 @@ program
105
110
  ...configTools,
106
111
  ...rubricTools,
107
112
  ...calendarTools,
108
- ...groupTools
113
+ ...groupTools,
114
+ ...enrollmentTools,
115
+ ...conversationTools,
116
+ ...newQuizTools,
117
+ ...analyticsTools,
118
+ ...peerReviewTools
109
119
  ];
110
120
  // --- Tool Handlers ---
111
121
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -163,6 +173,27 @@ program
163
173
  .action(async (options) => {
164
174
  const client = getClient();
165
175
  const port = Number.parseInt(options.port, 10);
166
- await startHttpServer(client, options.host, port);
176
+ const allTools = [
177
+ ...courseTools,
178
+ ...assignmentTools,
179
+ ...quizTools,
180
+ ...gradingTools,
181
+ ...communicationTools,
182
+ ...studentTools,
183
+ ...quizQuestionTools,
184
+ ...createTools,
185
+ ...moduleTools,
186
+ ...fileTools,
187
+ ...configTools,
188
+ ...rubricTools,
189
+ ...calendarTools,
190
+ ...groupTools,
191
+ ...enrollmentTools,
192
+ ...conversationTools,
193
+ ...newQuizTools,
194
+ ...analyticsTools,
195
+ ...peerReviewTools
196
+ ];
197
+ await startHttpServer(client, options.host, port, allTools);
167
198
  });
168
199
  program.parse(process.argv);
@@ -0,0 +1,214 @@
1
+ import { Ollama } from "ollama";
2
+ // ---------------------------------------------------------------------------
3
+ // Detección de modo
4
+ // ---------------------------------------------------------------------------
5
+ async function detectMode(ollama, model, sampleTool) {
6
+ try {
7
+ await ollama.chat({
8
+ model,
9
+ messages: [{ role: "user", content: "ping" }],
10
+ tools: [sampleTool],
11
+ stream: false
12
+ });
13
+ return "native";
14
+ }
15
+ catch (err) {
16
+ const msg = err instanceof Error ? err.message : String(err);
17
+ if (msg.includes("does not support tools"))
18
+ return "prompt";
19
+ throw err;
20
+ }
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // Parseo de tool calls en texto plano (modo prompt)
24
+ // ---------------------------------------------------------------------------
25
+ function parseTextToolCalls(content) {
26
+ const results = [];
27
+ const tagPattern = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
28
+ let match;
29
+ while ((match = tagPattern.exec(content)) !== null) {
30
+ try {
31
+ const parsed = JSON.parse(match[1]);
32
+ if (parsed.name && parsed.arguments) {
33
+ results.push({ name: parsed.name, arguments: parsed.arguments });
34
+ }
35
+ }
36
+ catch { /* ignorar */ }
37
+ }
38
+ if (results.length > 0)
39
+ return results;
40
+ // Fallback: JSON suelto con "name" + "arguments"
41
+ const jsonPattern = /\{[\s\S]*?"name"\s*:\s*"([^"]+)"[\s\S]*?"arguments"\s*:\s*(\{[\s\S]*?\})\s*\}/g;
42
+ while ((match = jsonPattern.exec(content)) !== null) {
43
+ try {
44
+ results.push({ name: match[1], arguments: JSON.parse(match[2]) });
45
+ }
46
+ catch { /* ignorar */ }
47
+ }
48
+ return results;
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // System prompt para modo prompt-engineering
52
+ // ---------------------------------------------------------------------------
53
+ const PROMPT_MODE_TOOLS = [
54
+ "canvas_list_courses",
55
+ "canvas_get_assignments",
56
+ "canvas_get_assignment",
57
+ "canvas_list_students",
58
+ "canvas_list_students_with_grades",
59
+ "canvas_get_student_grades",
60
+ "canvas_list_modules",
61
+ "canvas_list_quizzes",
62
+ "canvas_get_submissions",
63
+ "canvas_get_submission",
64
+ "canvas_grade_submission",
65
+ "canvas_list_announcements",
66
+ "canvas_list_discussions",
67
+ "canvas_audit_course",
68
+ "canvas_list_assignment_groups",
69
+ "canvas_list_assignment_due_dates",
70
+ "canvas_grade_multiple_submissions",
71
+ "canvas_get_submission_comments",
72
+ ];
73
+ function buildPromptSystemPrompt(tools) {
74
+ const selected = tools
75
+ .filter(t => PROMPT_MODE_TOOLS.includes(t.name))
76
+ .map(t => ({
77
+ name: t.tool.name,
78
+ description: t.tool.description,
79
+ parameters: t.tool.inputSchema
80
+ }));
81
+ return `Eres un asistente educativo con acceso a Canvas LMS.
82
+ Responde SIEMPRE en español. Sé conciso y claro.
83
+
84
+ Herramientas disponibles:
85
+ ${JSON.stringify(selected, null, 2)}
86
+
87
+ REGLAS:
88
+ 1. Para consultar Canvas, responde SOLO con un bloque <tool_call>:
89
+ <tool_call>
90
+ {"name": "nombre_herramienta", "arguments": {"param": "valor"}}
91
+ </tool_call>
92
+ 2. No escribas nada más cuando hagas una tool call.
93
+ 3. Después de recibir el resultado, responde al usuario en español.
94
+ 4. Una herramienta a la vez.`;
95
+ }
96
+ const NATIVE_SYSTEM = "Eres un asistente educativo con acceso a Canvas LMS. " +
97
+ "Usa las herramientas disponibles para responder sobre cursos, tareas, estudiantes y calificaciones. " +
98
+ "Responde SIEMPRE en español. Sé conciso y claro.";
99
+ // ---------------------------------------------------------------------------
100
+ // AgentRunner
101
+ // ---------------------------------------------------------------------------
102
+ export class AgentRunner {
103
+ ollama;
104
+ tools;
105
+ client;
106
+ ollamaTools;
107
+ constructor(client, tools, ollamaHost = "http://localhost:11434") {
108
+ this.client = client;
109
+ this.tools = tools;
110
+ this.ollama = new Ollama({ host: ollamaHost });
111
+ this.ollamaTools = tools.map(t => ({
112
+ type: "function",
113
+ function: {
114
+ name: t.tool.name,
115
+ description: t.tool.description ?? "",
116
+ parameters: t.tool.inputSchema
117
+ }
118
+ }));
119
+ }
120
+ async run(userMessage, options = {}) {
121
+ const modelRaw = options.model ?? "qwen2.5";
122
+ const modeArg = options.mode ?? "auto";
123
+ // Resolver nombre del modelo (fallback a :latest)
124
+ let model = modelRaw;
125
+ try {
126
+ await this.ollama.show({ model });
127
+ }
128
+ catch {
129
+ const withLatest = model.includes(":") ? model : `${model}:latest`;
130
+ try {
131
+ await this.ollama.show({ model: withLatest });
132
+ model = withLatest;
133
+ }
134
+ catch {
135
+ throw new Error(`Modelo '${modelRaw}' no encontrado en Ollama. Descárgalo con: ollama pull ${modelRaw}`);
136
+ }
137
+ }
138
+ // Detectar modo
139
+ let mode;
140
+ if (modeArg === "auto") {
141
+ mode = await detectMode(this.ollama, model, this.ollamaTools[0]);
142
+ }
143
+ else {
144
+ mode = modeArg;
145
+ }
146
+ const defaultSystem = mode === "prompt"
147
+ ? buildPromptSystemPrompt(this.tools)
148
+ : NATIVE_SYSTEM;
149
+ const messages = [{
150
+ role: "system",
151
+ content: options.systemPrompt ?? defaultSystem
152
+ }];
153
+ messages.push({ role: "user", content: userMessage });
154
+ const toolsUsed = [];
155
+ // Agentic loop (máx. 10 iteraciones para evitar loops infinitos)
156
+ for (let i = 0; i < 10; i++) {
157
+ const response = await this.ollama.chat({
158
+ model,
159
+ messages,
160
+ ...(mode === "native" ? { tools: this.ollamaTools } : {}),
161
+ stream: false
162
+ });
163
+ const assistantMessage = response.message;
164
+ messages.push(assistantMessage);
165
+ // Extraer tool calls
166
+ let calls = [];
167
+ if (mode === "native" && assistantMessage.tool_calls?.length) {
168
+ calls = assistantMessage.tool_calls.map(tc => ({
169
+ name: tc.function.name,
170
+ arguments: (tc.function.arguments ?? {})
171
+ }));
172
+ }
173
+ else if (mode === "prompt" && assistantMessage.content) {
174
+ calls = parseTextToolCalls(assistantMessage.content);
175
+ }
176
+ // Sin tool calls → respuesta final
177
+ if (calls.length === 0) {
178
+ const answer = (assistantMessage.content ?? "")
179
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "")
180
+ .trim();
181
+ return { answer, tools_used: toolsUsed, model, mode };
182
+ }
183
+ // Ejecutar cada herramienta
184
+ for (const call of calls) {
185
+ const toolDef = this.tools.find(t => t.name === call.name);
186
+ let resultText;
187
+ if (!toolDef) {
188
+ resultText = `Error: herramienta '${call.name}' no existe.`;
189
+ }
190
+ else {
191
+ try {
192
+ const result = await toolDef.handler(this.client, call.arguments);
193
+ const blocks = result?.content ?? [];
194
+ resultText = blocks
195
+ .filter((b) => b.type === "text" && b.text)
196
+ .map((b) => b.text)
197
+ .join("\n");
198
+ toolsUsed.push(call.name);
199
+ }
200
+ catch (err) {
201
+ resultText = `Error: ${err instanceof Error ? err.message : String(err)}`;
202
+ }
203
+ }
204
+ messages.push({
205
+ role: mode === "native" ? "tool" : "user",
206
+ content: mode === "native"
207
+ ? `[${call.name}]\n${resultText}`
208
+ : `Resultado de ${call.name}:\n${resultText}`
209
+ });
210
+ }
211
+ }
212
+ throw new Error("El agente excedió el límite de iteraciones (10).");
213
+ }
214
+ }
@@ -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
  }