# 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 |
```