Skip to main content
Glama
README.md
# moodle-mcp

> Model Context Protocol (MCP) server for Moodle. Lets AI agents publish and manage pedagogical content — lessons, resources, activities — in Moodle via Web Services with guaranteed idempotency.

[![CI](https://github.com/marcosnahuel/moodle-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/marcosnahuel/moodle-mcp/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/@nahuelalbornoz/moodle-mcp.svg)](https://www.npmjs.com/package/@nahuelalbornoz/moodle-mcp)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)

**Status:** wrapper **v0.5.2** + plugin **v0.5.0** — operate Moodle at ~80% from an LLM agent. (v0.5.0 release adds `add_questions_gift` contract fix; see CHANGELOG.)

---

## What it is

`moodle-mcp` is a stdio-based MCP server that exposes **40 high-level facades** plus one low-level `ws_raw` escape hatch to run almost the entire teaching workflow of a Moodle course from an AI agent: courses, sections, content (pages, urls, assignments, forums, videos), quizzes (with GIFT import), students (enrolment CSV, groups, roles, passwords), gradebook, messaging, calendar, and badges. Every write is upsert-by-`idnumber`, so republishing the same Ficha never creates duplicates.

Primary consumer: Claude Desktop driving the [Italicia](https://italicia.com) language-teaching workflow. But it is a generic open-source adapter — any MCP-capable agent + any Moodle 4.x/5.x instance with Web Services enabled can use it.

> **Plugin companion required for full feature set.** Upsert of `page`, `url`, `assign`, `quiz`, file uploads and GIFT imports go through the small PHP plugin `local_italiciamcp` that ships alongside this repo in `plugin-companion/`. Install it once (as "Complemento local" in the Moodle admin UI) and expose its functions to the external service the token belongs to. Without the plugin, the read-only facades (list students, grades, calendar, site info) and `ws_raw` still work.

## Tool catalog (40 tools + `ws_raw`)

Grouped by domain family. Each family lives under `src/tools/<family>/`.

### Curso
`crear_curso` · `actualizar_curso` · `duplicar_curso` · `archivar_curso` · `listar_mis_cursos` · `obtener_contexto_curso`

### Secciones
`crear_seccion` · `actualizar_seccion` · `ocultar_seccion` · `liberar_seccion` · `reordenar_secciones`

### Contenido
`publicar_ficha_clase` · `publicar_preview` · `confirmar_preview` · `generate_video` (Gemini Veo)

### Evaluación (quizzes)
`publicar_ficha_examen` (one-shot) · `configurar_quiz` · `importar_gift`

### Alumnos
`listar_alumnos` · `matricular_csv` · `dar_baja` · `crear_grupo` · `asignar_a_grupo` · `cambiar_rol` · `reset_password`

### Gradebook
`obtener_calificaciones` · `obtener_completion` · `obtener_intentos_quiz` · `obtener_entregas_assign` · `calificar_manualmente`

### Comunicación
`enviar_mensaje_moodle` · `crear_anuncio_foro` · `obtener_logs_curso` · `obtener_info_sitio`

### Calendario
`crear_evento_calendario` · `listar_eventos_calendario` · `actualizar_evento` · `eliminar_evento`

### Badges (read-only)
`listar_badges_usuario`

### Primitive
`ws_raw` — call any Moodle WS function directly when no facade covers the case.

### Deferred to v0.6 (require new plugin endpoints)
`duplicar_seccion`, `crear_banco_preguntas`, `editar_preguntas_banco`, `liberar_quiz`, `ocultar_quiz`, `otorgar_badge`.

## Installation

```bash
# Via npx (recommended for Claude Desktop)
npx -y @nahuelalbornoz/moodle-mcp

# Or install globally
npm install -g @nahuelalbornoz/moodle-mcp
```

Requires Node.js 20 or higher.

## Configuration (env vars)

| Variable | Required | Default | Description |
|---|---|---|---|
| `MOODLE_URL` | yes | — | Full HTTPS URL of the Moodle instance. |
| `MOODLE_WS_TOKEN` | yes | — | Web Services token with edit permissions. |
| `MOODLE_WS_TIMEOUT_MS` | no | `30000` | Per-request timeout. |
| `MOODLE_WS_MAX_RETRIES` | no | `3` | Retry attempts on transient failures. |
| `MOODLE_WS_RATE_LIMIT_PER_SEC` | no | `10` | Token-bucket rate limit. |
| `MCP_LOG_LEVEL` | no | `info` | `error` / `warn` / `info` / `debug`. |
| `MOODLE_ALLOW_INSECURE` | no | `false` | Allow `http://` URLs (dev-only escape hatch). |

## Claude Desktop config

Add to `claude_desktop_config.json` (see [`examples/setup-claude-desktop.md`](./examples/setup-claude-desktop.md) for the exact path per OS):

```json
{
  "mcpServers": {
    "moodle": {
      "command": "npx",
      "args": ["-y", "moodle-mcp"],
      "env": {
        "MOODLE_URL": "https://your-moodle.example.com",
        "MOODLE_WS_TOKEN": "your-ws-token"
      }
    }
  }
}
```

Restart Claude Desktop. The five tools above should now be available to the agent.

## Examples

### 1. Snapshot a course before acting

```jsonc
// tool call
{
  "name": "obtener_contexto_curso",
  "arguments": { "course_id": 42, "incluir_ultimas_clases": 5 }
}
```

Response (abridged):

```json
{
  "course": { "id": 42, "fullname": "Italiano A1", "shortname": "ITA-A1", "format": "topics", "startdate": 1700000000 },
  "secciones": [{ "id": 100, "name": "Unidad 3", "section": 3, "visible": true, "modules_count": 6 }],
  "ultimas_clases": [{ "seccion_id": 100, "seccion_name": "Unidad 3", "ficha_idnumber": "mcp:a9993e364706816aba3e2571" }],
  "matriculados": { "total": 18, "docentes": 1, "alumnos": 17 }
}
```

### 2. Publish a FichaClase (preview first)

```jsonc
{
  "name": "publicar_preview",
  "arguments": {
    "ficha_path": "/home/alicia/fichas/italiano/a1-2026/u3/c5.md",
    "course_id": 42
  }
}
```

Response includes `preview_url` Alicia can open to review. Once approved:

```jsonc
{
  "name": "confirmar_preview",
  "arguments": { "seccion_id": 100, "recursos_ids": [501, 502, 503] }
}
```

### 3. Escape hatch — call a raw WS function

```jsonc
{
  "name": "ws_raw",
  "arguments": {
    "function_name": "core_webservice_get_site_info",
    "params": {}
  }
}
```

Response:

```json
{ "data": { "sitename": "Aula Italicia", "release": "5.0.2+", ... } }
```

## Idempotency

Every resource created by this MCP carries a stable `idnumber` of the form:

```
mcp:<first 24 chars of sha1(ficha.id + "|" + component_id)>
```

Republishing the same Ficha finds the existing resource by `idnumber` and updates it in place. Nothing gets duplicated. Safe to retry anywhere, anytime.

## v0.1 caveats

v0.1 is honest about its capability boundary. It reliably:

- Looks up a course, its sections and modules.
- Finds "owned" resources by the `mcp:` idnumber prefix.
- Updates visibility of pre-existing modules (the preview → confirm workflow).
- Surfaces structured Moodle errors with stable `code` fields.
- Never logs tokens, never propagates stack traces.

v0.1 does **not** yet:

- Upload asset files via multipart to the Moodle draft file area. Calls planned for asset upload are reported back in `advertencias` — seed them manually the first time.
- Create brand-new sections or modules through Web Services. Where a module does not exist yet, the tool returns status `"missing"` plus an `advertencia`. Installing [`local_wsmanagesections`](https://moodle.org/plugins/local_wsmanagesections) (or equivalent) and wiring those endpoints is v0.2 work.

Both gaps are driven out by the integration suite in `tests/integration/` when run against a real Moodle docker.

## Development

```bash
git clone https://github.com/marcosnahuel/moodle-mcp
cd moodle-mcp
npm install

npm run typecheck         # tsc --noEmit
npm test                  # vitest unit suite
npm run test:coverage     # with v8 coverage (≥80% enforced)
npm run build             # tsup → dist/

# Integration — requires docker
docker compose -f tests/integration/docker-compose.test.yml up -d
export MOODLE_TEST_URL=http://localhost:8081
export MOODLE_TEST_TOKEN=<generate in Moodle admin>
export MOODLE_TEST_COURSE=<course id>
npm run test:integration
docker compose -f tests/integration/docker-compose.test.yml down -v
```

## Security

- The token is never logged. Tokens appearing in any field of any log record are replaced with `***`.
- URLs in error messages are likewise redacted.
- HTTPS is required unless `MOODLE_ALLOW_INSECURE=true` (dev-only).
- The MCP only talks to Moodle via Web Services REST. No cookie auth, no web scraping, no direct DB access.

## Contributing

See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for issue, PR and commit conventions.

By participating in this project you agree to abide by the [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md).

## License

MIT © 2026 Italicia — see [`LICENSE`](./LICENSE).

TDQS

A3.9/5.0

Scored across 5 tools

Disambiguation5/5

Each tool has a distinct and non-overlapping purpose: obtener_contexto_curso provides course metadata, publicar_ficha_clase publishes a class file, publicar_preview publishes in hidden mode, confirmar_preview makes previews visible, and ws_raw serves as a low-level escape hatch. The descriptions clearly differentiate their roles, with no ambiguity in selection.

Naming Consistency2/5

Naming is inconsistent with mixed conventions: obtener_contexto_curso and publicar_ficha_clase use Spanish verbs with snake_case, while confirmar_preview and publicar_preview mix Spanish verbs with English terms, and ws_raw is an English abbreviation. There is no uniform pattern across the tool set, making it chaotic and harder to predict.

Tool Count5/5

With 5 tools, the count is well-scoped for the server's purpose of managing Moodle courses. Each tool serves a specific function in the publishing workflow (e.g., preview, confirmation, raw access), and none feel redundant or missing for the apparent scope, making the set appropriately sized.

Completeness4/5

The tool set covers core workflows for publishing and managing Moodle course content, including metadata retrieval, publishing with preview options, and confirmation. A minor gap exists in lacking direct update or deletion tools for existing content, but agents can work around this using the idempotent publishing tools and the ws_raw escape hatch for other operations.

Maintenance

ActivityInactive
ResponsivenessUnresponsive