#
tokens: 7328/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Moodle MCP Server
  2 | 
  3 | An MCP (Model Context Protocol) server that enables LLMs to interact with the Moodle platform to manage courses, students, assignments, and quizzes.
  4 | 
  5 | ## Features
  6 | 
  7 | ### Student Management Tools
  8 | - `list_students` - Retrieves the list of students enrolled in the course
  9 |   - Displays ID, name, email, and last access time for each student
 10 | 
 11 | ### Assignment Management Tools
 12 | - `get_assignments` - Retrieves all available assignments in the course
 13 |   - Includes information such as ID, name, description, due date, and maximum grade
 14 | - `get_student_submissions` - Examines a student's submissions for a specific assignment
 15 |   - Requires the assignment ID and optionally the student ID
 16 | - `provide_assignment_feedback` - Provides grades and comments for a student's submission
 17 |   - Requires student ID, assignment ID, grade, and feedback comment
 18 | 
 19 | ### Quiz Management Tools
 20 | - `get_quizzes` - Retrieves all available quizzes in the course
 21 |   - Includes information such as ID, name, description, opening/closing dates, and maximum grade
 22 | - `get_quiz_attempts` - Examines a student's attempts on a specific quiz
 23 |   - Requires the quiz ID and optionally the student ID
 24 | - `provide_quiz_feedback` - Provides comments for a quiz attempt
 25 |   - Requires the attempt ID and feedback comment
 26 | 
 27 | ## Requirements
 28 | 
 29 | - Node.js (v14 or higher)
 30 | - Moodle API token with appropriate permissions
 31 | - Moodle course ID
 32 | 
 33 | ## Installation
 34 | 
 35 | 1. Clone this repository:
 36 | ```bash
 37 | git clone https://github.com/your-username/moodle-mcp-server.git
 38 | cd moodle-mcp-server
 39 | ```
 40 | 
 41 | 2. Install dependencies:
 42 | ```bash
 43 | npm install
 44 | ```
 45 | 
 46 | 3. Create a `.env` file with the following configuration:
 47 | ```
 48 | MOODLE_API_URL=https://your-moodle.com/webservice/rest/server.php
 49 | MOODLE_API_TOKEN=your_api_token
 50 | MOODLE_COURSE_ID=1  # Replace with your course ID
 51 | ```
 52 | 
 53 | 4. Build the server:
 54 | ```bash
 55 | npm run build
 56 | ```
 57 | 
 58 | ## Usage with Claude
 59 | 
 60 | To use with Claude Desktop, add the server configuration:
 61 | 
 62 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`  
 63 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
 64 | 
 65 | ```json
 66 | {
 67 |   "mcpServers": {
 68 |     "moodle-mcp-server": {
 69 |       "command": "/path/to/node",
 70 |       "args": [
 71 |         "/path/to/moodle-mcp-server/build/index.js"
 72 |       ],
 73 |       "env": {
 74 |         "MOODLE_API_URL": "https://your-moodle.com/webservice/rest/server.php",
 75 |         "MOODLE_API_TOKEN": "your_moodle_api_token",
 76 |         "MOODLE_COURSE_ID": "your_course_id"
 77 |       },
 78 |       "disabled": false,
 79 |       "autoApprove": []
 80 |     }
 81 |   }
 82 | }
 83 | ```
 84 | 
 85 | For Windows users, the paths would use backslashes:
 86 | 
 87 | ```json
 88 | {
 89 |   "mcpServers": {
 90 |     "moodle-mcp-server": {
 91 |       "command": "C:\\path\\to\\node.exe",
 92 |       "args": [
 93 |         "C:\\path\\to\\moodle-mcp-server\\build\\index.js"
 94 |       ],
 95 |       "env": {
 96 |         "MOODLE_API_URL": "https://your-moodle.com/webservice/rest/server.php",
 97 |         "MOODLE_API_TOKEN": "your_moodle_api_token",
 98 |         "MOODLE_COURSE_ID": "your_course_id"
 99 |       },
100 |       "disabled": false,
101 |       "autoApprove": []
102 |     }
103 |   }
104 | }
105 | ```
106 | 
107 | Once configured, Claude will be able to interact with your Moodle course to:
108 | - View the list of students and their submissions
109 | - Provide comments and grades for assignments
110 | - Examine quiz attempts and offer feedback
111 | 
112 | ## Development
113 | 
114 | For development with auto-rebuild:
115 | ```bash
116 | npm run watch
117 | ```
118 | 
119 | ### Debugging
120 | 
121 | MCP servers communicate through stdio, which can make debugging challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
122 | 
123 | ```bash
124 | npm run inspector
125 | ```
126 | 
127 | The Inspector will provide a URL to access debugging tools in your browser.
128 | 
129 | ## Obtaining a Moodle API Token
130 | 
131 | 1. Log in to your Moodle site as an administrator
132 | 2. Go to Site Administration > Plugins > Web Services > Manage tokens
133 | 3. Create a new token with the necessary permissions to manage courses
134 | 4. Copy the generated token and add it to your `.env` file
135 | 
136 | ## Security
137 | 
138 | - Never share your `.env` file or Moodle API token
139 | - Ensure the MCP server only has access to the courses it needs to manage
140 | - Use a token with the minimum necessary permissions
141 | 
142 | ## License
143 | 
144 | [MIT](LICENSE)
145 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "moodle-mcp-server",
 3 |   "version": "0.1.0",
 4 |   "description": "Moddle MCP server",
 5 |   "private": true,
 6 |   "type": "module",
 7 |   "bin": {
 8 |     "moodle-mcp-server": "./build/index.js"
 9 |   },
10 |   "files": [
11 |     "build"
12 |   ],
13 |   "scripts": {
14 |     "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
15 |     "prepare": "npm run build",
16 |     "watch": "tsc --watch",
17 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js"
18 |   },
19 |   "dependencies": {
20 |     "@modelcontextprotocol/sdk": "0.6.0",
21 |     "axios": "^1.8.2"
22 |   },
23 |   "devDependencies": {
24 |     "@types/node": "^20.11.24",
25 |     "typescript": "^5.3.3"
26 |   }
27 | }
28 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  4 | import {
  5 |   CallToolRequestSchema,
  6 |   ErrorCode,
  7 |   ListToolsRequestSchema,
  8 |   McpError,
  9 | } from '@modelcontextprotocol/sdk/types.js';
 10 | import axios from 'axios';
 11 | 
 12 | // Configuración de variables de entorno
 13 | const MOODLE_API_URL = process.env.MOODLE_API_URL;
 14 | const MOODLE_API_TOKEN = process.env.MOODLE_API_TOKEN;
 15 | const MOODLE_COURSE_ID = process.env.MOODLE_COURSE_ID;
 16 | 
 17 | // Verificar que las variables de entorno estén definidas
 18 | if (!MOODLE_API_URL) {
 19 |   throw new Error('MOODLE_API_URL environment variable is required');
 20 | }
 21 | 
 22 | if (!MOODLE_API_TOKEN) {
 23 |   throw new Error('MOODLE_API_TOKEN environment variable is required');
 24 | }
 25 | 
 26 | if (!MOODLE_COURSE_ID) {
 27 |   throw new Error('MOODLE_COURSE_ID environment variable is required');
 28 | }
 29 | 
 30 | // Interfaces para los tipos de datos
 31 | interface Student {
 32 |   id: number;
 33 |   username: string;
 34 |   firstname: string;
 35 |   lastname: string;
 36 |   email: string;
 37 | }
 38 | 
 39 | interface Assignment {
 40 |   id: number;
 41 |   name: string;
 42 |   duedate: number;
 43 |   allowsubmissionsfromdate: number;
 44 |   grade: number;
 45 |   timemodified: number;
 46 |   cutoffdate: number;
 47 | }
 48 | 
 49 | interface Quiz {
 50 |   id: number;
 51 |   name: string;
 52 |   timeopen: number;
 53 |   timeclose: number;
 54 |   grade: number;
 55 |   timemodified: number;
 56 | }
 57 | 
 58 | interface Submission {
 59 |   id: number;
 60 |   userid: number;
 61 |   status: string;
 62 |   timemodified: number;
 63 |   gradingstatus: string;
 64 |   gradefordisplay?: string;
 65 | }
 66 | 
 67 | interface SubmissionContent {
 68 |   assignment: number;
 69 |   userid: number;
 70 |   status: string;
 71 |   submissiontext?: string;
 72 |   plugins?: Array<{
 73 |     type: string;
 74 |     content?: string;
 75 |     files?: Array<{
 76 |       filename: string;
 77 |       fileurl: string;
 78 |       filesize: number;
 79 |       filetype: string;
 80 |     }>;
 81 |   }>;
 82 |   timemodified: number;
 83 | }
 84 | 
 85 | interface QuizGradeResponse {
 86 |   hasgrade: boolean;
 87 |   grade?: string;  // Este campo solo está presente si hasgrade es true
 88 | }
 89 | 
 90 | class MoodleMcpServer {
 91 |   private server: Server;
 92 |   private axiosInstance;
 93 | 
 94 |   constructor() {
 95 |     this.server = new Server(
 96 |       {
 97 |         name: 'moodle-mcp-server',
 98 |         version: '0.1.0',
 99 |       },
100 |       {
101 |         capabilities: {
102 |           tools: {},
103 |         },
104 |       }
105 |     );
106 | 
107 |     this.axiosInstance = axios.create({
108 |       baseURL: MOODLE_API_URL,
109 |       params: {
110 |         wstoken: MOODLE_API_TOKEN,
111 |         moodlewsrestformat: 'json',
112 |       },
113 |     });
114 | 
115 |     this.setupToolHandlers();
116 |     
117 |     // Error handling
118 |     this.server.onerror = (error) => console.error('[MCP Error]', error);
119 |     process.on('SIGINT', async () => {
120 |       await this.server.close();
121 |       process.exit(0);
122 |     });
123 |   }
124 | 
125 |   private setupToolHandlers() {
126 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
127 |       tools: [
128 |         {
129 |           name: 'get_students',
130 |           description: 'Obtiene la lista de estudiantes inscritos en el curso configurado',
131 |           inputSchema: {
132 |             type: 'object',
133 |             properties: {},
134 |             required: [],
135 |           },
136 |         },
137 |         {
138 |           name: 'get_assignments',
139 |           description: 'Obtiene la lista de tareas asignadas en el curso configurado',
140 |           inputSchema: {
141 |             type: 'object',
142 |             properties: {},
143 |             required: [],
144 |           },
145 |         },
146 |         {
147 |           name: 'get_quizzes',
148 |           description: 'Obtiene la lista de quizzes en el curso configurado',
149 |           inputSchema: {
150 |             type: 'object',
151 |             properties: {},
152 |             required: [],
153 |           },
154 |         },
155 |         {
156 |           name: 'get_submissions',
157 |           description: 'Obtiene las entregas de tareas en el curso configurado',
158 |           inputSchema: {
159 |             type: 'object',
160 |             properties: {
161 |               studentId: {
162 |                 type: 'number',
163 |                 description: 'ID opcional del estudiante. Si no se proporciona, se devolverán entregas de todos los estudiantes',
164 |               },
165 |               assignmentId: {
166 |                 type: 'number',
167 |                 description: 'ID opcional de la tarea. Si no se proporciona, se devolverán todas las entregas',
168 |               },
169 |             },
170 |             required: [],
171 |           },
172 |         },
173 |         {
174 |           name: 'provide_feedback',
175 |           description: 'Proporciona feedback sobre una tarea entregada por un estudiante',
176 |           inputSchema: {
177 |             type: 'object',
178 |             properties: {
179 |               studentId: {
180 |                 type: 'number',
181 |                 description: 'ID del estudiante',
182 |               },
183 |               assignmentId: {
184 |                 type: 'number',
185 |                 description: 'ID de la tarea',
186 |               },
187 |               grade: {
188 |                 type: 'number',
189 |                 description: 'Calificación numérica a asignar',
190 |               },
191 |               feedback: {
192 |                 type: 'string',
193 |                 description: 'Texto del feedback a proporcionar',
194 |               },
195 |             },
196 |             required: ['studentId', 'assignmentId', 'feedback'],
197 |           },
198 |         },
199 |         {
200 |           name: 'get_submission_content',
201 |           description: 'Obtiene el contenido detallado de una entrega específica, incluyendo texto y archivos adjuntos',
202 |           inputSchema: {
203 |             type: 'object',
204 |             properties: {
205 |               studentId: {
206 |                 type: 'number',
207 |                 description: 'ID del estudiante',
208 |               },
209 |               assignmentId: {
210 |                 type: 'number',
211 |                 description: 'ID de la tarea',
212 |               },
213 |             },
214 |             required: ['studentId', 'assignmentId'],
215 |           },
216 |         },
217 |         {
218 |           name: 'get_quiz_grade',
219 |           description: 'Obtiene la calificación de un estudiante en un quiz específico',
220 |           inputSchema: {
221 |             type: 'object',
222 |             properties: {
223 |               studentId: {
224 |                 type: 'number',
225 |                 description: 'ID del estudiante',
226 |               },
227 |               quizId: {
228 |                 type: 'number',
229 |                 description: 'ID del quiz',
230 |               },
231 |             },
232 |             required: ['studentId', 'quizId'],
233 |           },
234 |         },
235 |       ],
236 |     }));
237 | 
238 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
239 |       console.error(`[Tool] Executing tool: ${request.params.name}`);
240 |       
241 |       try {
242 |         switch (request.params.name) {
243 |           case 'get_students':
244 |             return await this.getStudents();
245 |           case 'get_assignments':
246 |             return await this.getAssignments();
247 |           case 'get_quizzes':
248 |             return await this.getQuizzes();
249 |           case 'get_submissions':
250 |             return await this.getSubmissions(request.params.arguments);
251 |           case 'provide_feedback':
252 |             return await this.provideFeedback(request.params.arguments);
253 |           case 'get_submission_content':
254 |             return await this.getSubmissionContent(request.params.arguments);
255 |           case 'get_quiz_grade':
256 |             return await this.getQuizGrade(request.params.arguments);
257 |           default:
258 |             throw new McpError(
259 |               ErrorCode.MethodNotFound,
260 |               `Unknown tool: ${request.params.name}`
261 |             );
262 |         }
263 |       } catch (error) {
264 |         console.error('[Error]', error);
265 |         if (axios.isAxiosError(error)) {
266 |           return {
267 |             content: [
268 |               {
269 |                 type: 'text',
270 |                 text: `Moodle API error: ${
271 |                   error.response?.data?.message || error.message
272 |                 }`,
273 |               },
274 |             ],
275 |             isError: true,
276 |           };
277 |         }
278 |         throw error;
279 |       }
280 |     });
281 |   }
282 | 
283 |   private async getStudents() {
284 |     console.error('[API] Requesting enrolled users');
285 |     
286 |     const response = await this.axiosInstance.get('', {
287 |       params: {
288 |         wsfunction: 'core_enrol_get_enrolled_users',
289 |         courseid: MOODLE_COURSE_ID,
290 |       },
291 |     });
292 | 
293 |     const students = response.data
294 |       .filter((user: any) => user.roles.some((role: any) => role.shortname === 'student'))
295 |       .map((student: any) => ({
296 |         id: student.id,
297 |         username: student.username,
298 |         firstname: student.firstname,
299 |         lastname: student.lastname,
300 |         email: student.email,
301 |       }));
302 | 
303 |     return {
304 |       content: [
305 |         {
306 |           type: 'text',
307 |           text: JSON.stringify(students, null, 2),
308 |         },
309 |       ],
310 |     };
311 |   }
312 | 
313 |   private async getAssignments() {
314 |     console.error('[API] Requesting assignments');
315 |     
316 |     const response = await this.axiosInstance.get('', {
317 |       params: {
318 |         wsfunction: 'mod_assign_get_assignments',
319 |         courseids: [MOODLE_COURSE_ID],
320 |       },
321 |     });
322 | 
323 |     const assignments = response.data.courses[0]?.assignments || [];
324 |     
325 |     return {
326 |       content: [
327 |         {
328 |           type: 'text',
329 |           text: JSON.stringify(assignments, null, 2),
330 |         },
331 |       ],
332 |     };
333 |   }
334 | 
335 |   private async getQuizzes() {
336 |     console.error('[API] Requesting quizzes');
337 |     
338 |     const response = await this.axiosInstance.get('', {
339 |       params: {
340 |         wsfunction: 'mod_quiz_get_quizzes_by_courses',
341 |         courseids: [MOODLE_COURSE_ID],
342 |       },
343 |     });
344 | 
345 |     const quizzes = response.data.quizzes || [];
346 |     
347 |     return {
348 |       content: [
349 |         {
350 |           type: 'text',
351 |           text: JSON.stringify(quizzes, null, 2),
352 |         },
353 |       ],
354 |     };
355 |   }
356 | 
357 |   private async getSubmissions(args: any) {
358 |     const studentId = args.studentId;
359 |     const assignmentId = args.assignmentId;
360 |     
361 |     console.error(`[API] Requesting submissions${studentId ? ` for student ${studentId}` : ''}`);
362 |     
363 |     // Primero obtenemos todas las tareas
364 |     const assignmentsResponse = await this.axiosInstance.get('', {
365 |       params: {
366 |         wsfunction: 'mod_assign_get_assignments',
367 |         courseids: [MOODLE_COURSE_ID],
368 |       },
369 |     });
370 | 
371 |     const assignments = assignmentsResponse.data.courses[0]?.assignments || [];
372 |     
373 |     // Si se especificó un ID de tarea, filtramos solo esa tarea
374 |     const targetAssignments = assignmentId
375 |       ? assignments.filter((a: any) => a.id === assignmentId)
376 |       : assignments;
377 |     
378 |     if (targetAssignments.length === 0) {
379 |       return {
380 |         content: [
381 |           {
382 |             type: 'text',
383 |             text: 'No se encontraron tareas para el criterio especificado.',
384 |           },
385 |         ],
386 |       };
387 |     }
388 | 
389 |     // Para cada tarea, obtenemos todas las entregas
390 |     const submissionsPromises = targetAssignments.map(async (assignment: any) => {
391 |       const submissionsResponse = await this.axiosInstance.get('', {
392 |         params: {
393 |           wsfunction: 'mod_assign_get_submissions',
394 |           assignmentids: [assignment.id],
395 |         },
396 |       });
397 | 
398 |       const submissions = submissionsResponse.data.assignments[0]?.submissions || [];
399 |       
400 |       // Obtenemos las calificaciones para esta tarea
401 |       const gradesResponse = await this.axiosInstance.get('', {
402 |         params: {
403 |           wsfunction: 'mod_assign_get_grades',
404 |           assignmentids: [assignment.id],
405 |         },
406 |       });
407 | 
408 |       const grades = gradesResponse.data.assignments[0]?.grades || [];
409 |       
410 |       // Si se especificó un ID de estudiante, filtramos solo sus entregas
411 |       const targetSubmissions = studentId
412 |         ? submissions.filter((s: any) => s.userid === studentId)
413 |         : submissions;
414 |       
415 |       // Procesamos cada entrega
416 |       const processedSubmissions = targetSubmissions.map((submission: any) => {
417 |         const studentGrade = grades.find((g: any) => g.userid === submission.userid);
418 |         
419 |         return {
420 |           userid: submission.userid,
421 |           status: submission.status,
422 |           timemodified: new Date(submission.timemodified * 1000).toISOString(),
423 |           grade: studentGrade ? studentGrade.grade : 'No calificado',
424 |         };
425 |       });
426 |       
427 |       return {
428 |         assignment: assignment.name,
429 |         assignmentId: assignment.id,
430 |         submissions: processedSubmissions.length > 0 ? processedSubmissions : 'No hay entregas',
431 |       };
432 |     });
433 | 
434 |     const results = await Promise.all(submissionsPromises);
435 |     
436 |     return {
437 |       content: [
438 |         {
439 |           type: 'text',
440 |           text: JSON.stringify(results, null, 2),
441 |         },
442 |       ],
443 |     };
444 |   }
445 | 
446 |   private async provideFeedback(args: any) {
447 |     if (!args.studentId || !args.assignmentId || !args.feedback) {
448 |       throw new McpError(
449 |         ErrorCode.InvalidParams,
450 |         'Student ID, Assignment ID, and feedback are required'
451 |       );
452 |     }
453 | 
454 |     console.error(`[API] Providing feedback for student ${args.studentId} on assignment ${args.assignmentId}`);
455 |     
456 |     const response = await this.axiosInstance.get('', {
457 |       params: {
458 |         wsfunction: 'mod_assign_save_grade',
459 |         assignmentid: args.assignmentId,
460 |         userid: args.studentId,
461 |         grade: args.grade || 0,
462 |         attemptnumber: -1, // Último intento
463 |         addattempt: 0,
464 |         workflowstate: 'released',
465 |         applytoall: 0,
466 |         plugindata: {
467 |           assignfeedbackcomments_editor: {
468 |             text: args.feedback,
469 |             format: 1, // Formato HTML
470 |           },
471 |         },
472 |       },
473 |     });
474 | 
475 |     return {
476 |       content: [
477 |         {
478 |           type: 'text',
479 |           text: `Feedback proporcionado correctamente para el estudiante ${args.studentId} en la tarea ${args.assignmentId}.`,
480 |         },
481 |       ],
482 |     };
483 |   }
484 | 
485 |   private async getSubmissionContent(args: any) {
486 |     if (!args.studentId || !args.assignmentId) {
487 |       throw new McpError(
488 |         ErrorCode.InvalidParams,
489 |         'Student ID and Assignment ID are required'
490 |       );
491 |     }
492 | 
493 |     console.error(`[API] Requesting submission content for student ${args.studentId} on assignment ${args.assignmentId}`);
494 |     
495 |     try {
496 |       // Utilizamos la función mod_assign_get_submission_status para obtener el contenido detallado
497 |       const response = await this.axiosInstance.get('', {
498 |         params: {
499 |           wsfunction: 'mod_assign_get_submission_status',
500 |           assignid: args.assignmentId,
501 |           userid: args.studentId,
502 |         },
503 |       });
504 | 
505 |       // Procesamos la respuesta para extraer el contenido relevante
506 |       const submissionData = response.data.submission || {};
507 |       const plugins = response.data.lastattempt?.submission?.plugins || [];
508 |       
509 |       // Extraemos el texto de la entrega y los archivos adjuntos
510 |       let submissionText = '';
511 |       const files = [];
512 |       
513 |       for (const plugin of plugins) {
514 |         // Procesamos el plugin de texto en línea
515 |         if (plugin.type === 'onlinetext') {
516 |           const textField = plugin.editorfields?.find((field: any) => field.name === 'onlinetext');
517 |           if (textField) {
518 |             submissionText = textField.text || '';
519 |           }
520 |         }
521 |         
522 |         // Procesamos el plugin de archivos
523 |         if (plugin.type === 'file') {
524 |           const filesList = plugin.fileareas?.find((area: any) => area.area === 'submission_files');
525 |           if (filesList && filesList.files) {
526 |             for (const file of filesList.files) {
527 |               files.push({
528 |                 filename: file.filename,
529 |                 fileurl: file.fileurl,
530 |                 filesize: file.filesize,
531 |                 filetype: file.mimetype,
532 |               });
533 |             }
534 |           }
535 |         }
536 |       }
537 |       
538 |       // Construimos el objeto de respuesta
539 |       const submissionContent = {
540 |         assignment: args.assignmentId,
541 |         userid: args.studentId,
542 |         status: submissionData.status || 'unknown',
543 |         submissiontext: submissionText,
544 |         plugins: [
545 |           {
546 |             type: 'onlinetext',
547 |             content: submissionText,
548 |           },
549 |           {
550 |             type: 'file',
551 |             files: files,
552 |           },
553 |         ],
554 |         timemodified: submissionData.timemodified || 0,
555 |       };
556 |       
557 |       return {
558 |         content: [
559 |           {
560 |             type: 'text',
561 |             text: JSON.stringify(submissionContent, null, 2),
562 |           },
563 |         ],
564 |       };
565 |     } catch (error) {
566 |       console.error('[Error]', error);
567 |       if (axios.isAxiosError(error)) {
568 |         return {
569 |           content: [
570 |             {
571 |               type: 'text',
572 |               text: `Error al obtener el contenido de la entrega: ${
573 |                 error.response?.data?.message || error.message
574 |               }`,
575 |             },
576 |           ],
577 |           isError: true,
578 |         };
579 |       }
580 |       throw error;
581 |     }
582 |   }
583 | 
584 |   private async getQuizGrade(args: any) {
585 |     if (!args.studentId || !args.quizId) {
586 |       throw new McpError(
587 |         ErrorCode.InvalidParams,
588 |         'Student ID and Quiz ID are required'
589 |       );
590 |     }
591 | 
592 |     console.error(`[API] Requesting quiz grade for student ${args.studentId} on quiz ${args.quizId}`);
593 |     
594 |     try {
595 |       const response = await this.axiosInstance.get('', {
596 |         params: {
597 |           wsfunction: 'mod_quiz_get_user_best_grade',
598 |           quizid: args.quizId,
599 |           userid: args.studentId,
600 |         },
601 |       });
602 | 
603 |       // Procesamos la respuesta
604 |       const result = {
605 |         quizId: args.quizId,
606 |         studentId: args.studentId,
607 |         hasGrade: response.data.hasgrade,
608 |         grade: response.data.hasgrade ? response.data.grade : 'No calificado',
609 |       };
610 |       
611 |       return {
612 |         content: [
613 |           {
614 |             type: 'text',
615 |             text: JSON.stringify(result, null, 2),
616 |           },
617 |         ],
618 |       };
619 |     } catch (error) {
620 |       console.error('[Error]', error);
621 |       if (axios.isAxiosError(error)) {
622 |         return {
623 |           content: [
624 |             {
625 |               type: 'text',
626 |               text: `Error al obtener la calificación del quiz: ${
627 |                 error.response?.data?.message || error.message
628 |               }`,
629 |             },
630 |           ],
631 |           isError: true,
632 |         };
633 |       }
634 |       throw error;
635 |     }
636 |   }
637 | 
638 |   async run() {
639 |     const transport = new StdioServerTransport();
640 |     await this.server.connect(transport);
641 |     console.error('Moodle MCP server running on stdio');
642 |   }
643 | }
644 | 
645 | const server = new MoodleMcpServer();
646 | server.run().catch(console.error);
647 | 
```