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