moodle-ai-mcp

moodle-ai-mcp

Enables AI clients to get structured, read-only answers about a Moodle site: identity, capabilities, reachable external functions, and installed H5P libraries with JSON Schema conversions, for safe course and content inspection.

Category
Visit Server

README

moodle-ai-mcp

An AI-native MCP control plane for Moodle.

An MCP client (Claude Code, ChatGPT, Cursor, or anything else that speaks the Model Context Protocol) connects to this server and gets structured, accurate answers about a real Moodle site: what it is, who the connection is authenticated as, what it is allowed to do, which of Moodle's external functions it can reach, and — the part that makes it more than a REST wrapper — exactly which H5P libraries the site has installed and what their content schemas are.

This is not a thin wrapper around Moodle REST. The long-term goal is a control plane an AI client can use to design and build entire courses safely. This repository currently contains the first foundation of that.

Current maturity: read-side foundation, plus three narrow curated writes

Working today:

  • MCP server on stdio with ten curated tools, built on the official MCP TypeScript SDK
  • A Moodle 5.2 local plugin (local_aimcp) with thirteen external functions — ten read-declared and three narrow writes — real capability enforcement and PHPUnit coverage
  • A write-policy substrate every curated mutation passes through: plan and apply, optimistic concurrency against a locked row, durable idempotency and Moodle-native audit, all enforced inside Moodle — plus a cryptographic confirmation handshake enforced in the MCP server. Proven across three different targets in three different tables, each with its own capability
  • A capability-aware course read model: sections, activities, completion and grade configuration, reflecting what the authenticated identity may actually see rather than everything with a hidden flag attached
  • Dynamic discovery of the external functions the authenticated service can reach, with lossless signature introspection
  • Dynamic discovery of installed H5P libraries and their real installed semantics, converted to JSON Schema with explicit notes for everything JSON Schema cannot express

Not built, on purpose:

  • broad course or activity CRUD — course creation, and creating, deleting or moving sections and activities
  • arbitrary Moodle external-function execution, and any generic write executor
  • H5P authoring, and quiz or question-bank authoring
  • the Course Blueprint engine
  • browser QA and learner simulation
  • a remote HTTP transport with OAuth, and hosting infrastructure

The three writes that do exist are narrow, curated and individually allowlisted; each edits named fields of one existing object. See "Limitations" below.

Architecture

AI client  --MCP/stdio-->  apps/mcp-server (TypeScript, MIT)
                                 |
                                 |  authenticated Moodle web service call
                                 v
                           moodle/local/aimcp (Moodle plugin, GPL-3.0-or-later)
                                 |
                                 v
                           Moodle 5.2 core + H5P core

The server owns the protocol, the tool surface, orchestration and schema conversion. The plugin owns everything that only Moodle can answer: identity, context, capabilities, the external-function registry and the H5P engine. Orchestration never leaks into PHP, and the rule is that Moodle logic is not reimplemented in TypeScript — when PHP can decide something authoritatively, the server asks PHP. One place still falls short of that rule: the moodle_course_update_summary input schema caps the summary length and the format constant locally, which moodle_section_update_name deliberately does not do for its own limit.

Details, including why the tool surface is a curated handful rather than several hundred, are in docs/ARCHITECTURE.md.

Prerequisites

  • Node.js 24
  • Docker, with a Moodle 5.2 stack from moodle-docker
  • A Moodle web service token for a user authorised on an enabled external service

Local development

Full instructions: docs/LOCAL-DEV.md. The short version:

cd ~/DEV/moodle-ai/moodle-ai-mcp

# 1. Start the Moodle stack (installs the persistence override, mounts the plugin)
./scripts/stack.sh start

# 2. Register the plugin with Moodle
docker exec -u www-data -w /var/www/html moodle-ai-webserver-1 \
  php admin/cli/upgrade.php --non-interactive

# 3. Attach the plugin's functions to your external service (idempotent)
docker exec -u www-data -w /var/www/html moodle-ai-webserver-1 \
  php public/local/aimcp/cli/provision_service.php --service=moodle_ai_mcp_dev

# 4. Build and run the server
npm install
npm run build
./scripts/run-server.sh

The database, moodledata and installed H5P libraries live in named Docker volumes, so ./scripts/stack.sh recreate is safe. Only ./scripts/stack.sh reset destroys data, and it asks first. Back up any time with ./scripts/backup.sh.

Credentials come from .env.local, which is a symlink to a file outside this repository. .env* is gitignored; see docs/SECURITY.md.

Connecting an MCP client

claude mcp add moodle-ai --scope local -- \
  /absolute/path/to/moodle-ai-mcp/scripts/run-server.sh

Or with the Inspector:

npx @modelcontextprotocol/inspector ./scripts/run-server.sh

Tools

Tool What it answers
moodle_site_inspect What Moodle is this, who am I connected as, what can that identity do, what plugins and H5P are available.
moodle_course_list Which courses exist and are visible to this identity, optionally searched.
moodle_course_inspect The structure of one course: sections in order, activities in course-page order, completion configuration and grade item configuration. Omits what the caller may not see, gates course-management fields (raw availability rules, module ID numbers) behind Moodle's own editor capabilities, and says how much it withheld.
moodle_functions_search Which of Moodle's external functions can this connection reach, ranked by relevance. Discovered live, never from a built-in list.
moodle_functions_describe The full signature of one function: Moodle's own parameter and return tree, plus generated JSON Schema and conversion notes.
moodle_h5p_types Which H5P libraries are installed, at which exact versions, which are runnable content types, which are dependency-only, and which Moodle currently offers for authoring.
moodle_h5p_schema The installed semantics for one H5P library version, plus generated JSON Schema and notes for everything H5P expresses that JSON Schema cannot.
moodle_course_update_summary Writes. Replaces one course's summary. No length limit is imposed, because Moodle imposes none. The text format is Moodle's to accept or refuse, and it refuses at the planning step, so a format it could not render never receives a confirmation token.
moodle_section_update_name Writes. Renames one course section. An empty name clears the custom name, so the format default is shown again. HTML is refused rather than stripped, and refused at the planning step, so an invalid name never receives a confirmation token.
moodle_activity_set_visibility Writes. Sets one activity's structural show/hide state on the course page, taking its calendar events and grade items with it, exactly as Moodle does. Showing an activity does not by itself make it reachable — availability restrictions, dates, groups, completion prerequisites and course visibility still apply. It cannot set stealth mode, and will clear it. Permission is checked on the activity itself, so a per-activity override applies. Refuses while the containing section is hidden, because Moodle owns activity visibility there and would undo the change.

Each of the three writing tools is two phases of one tool rather than two tools. Called without a confirmation token it returns a plan and changes nothing; called with the token that plan issued, plus an idempotency key, it applies. A client cannot reach the apply phase without a token the plan phase gave it.

A plan can go stale, expire or find its target ineligible later, but it is never knowingly invalid when issued: every tool that carries a value sends it to Moodle at plan time, so a value Moodle would refuse never receives a token. What counts as refusable is Moodle's answer and not this project's — a section name has a length limit and must be plain text, a course summary has no length limit at all, and only its format can be unsupported.

Seven are annotated readOnlyHint: true. The three writes are annotated readOnlyHint: false and destructiveHint: true — MCP's false means the tool "performs only additive updates", and replacing a summary, a name or a visibility state is not additive. That is the protocol's question, and it is not the same as "can this be undone": the inverse call does restore the previous value for a summary or a rename, and does not for an activity that was in stealth mode, since this tool cannot express stealth and so cannot put it back. The plan reports this project's riskClass — destructive for activity visibility, write for the other two — and warns in words before the change is confirmed. reversible is a property of the operation registry and is documented in docs/SECURITY.md; it is not a field on the plan. All ten return structuredContent and a JSON text fallback on success; an error returns a structured JSON text block instead.

There is deliberately no generic "call any Moodle function" tool, and no generic write executor. Search and describe make the long tail discoverable and report whether Moodle declares each function read or write; nothing executes a function by name. Moodle's own write label is treated as a floor, not a safety verdict — it covers editing a description and deleting a course alike.

Tests

npm --prefix apps/mcp-server run typecheck      # TypeScript, strict
npm --prefix apps/mcp-server run test:unit      # pure logic, no Moodle needed
npm --prefix apps/mcp-server run build
npm --prefix apps/mcp-server run test:integration  # real Moodle + real MCP session

./scripts/lint-plugin.sh    # php -l over the plugin
./scripts/check-plugin.sh   # Moodle coding standard (moodle-cs)
./scripts/test-plugin.sh    # PHPUnit inside the Moodle container

The integration suite is not a mock: it spawns the built server as a child process, speaks MCP to it with the official SDK client, and asserts against the live site — including that the identity is the expected Moodle user and that no token appears in any output.

Limitations

  • moodle_activity_set_visibility needs a visible parent section. While a section is hidden Moodle forces every activity in it to hidden and restores the previous value when the section is shown again, so an activity-level change there would not last. The tool returns unsupported_target_state rather than reporting a success a later section unhide would contradict. Section visibility itself is not modelled in this milestone.
  • Three narrow writes. A course summary, a section name, and an activity's visibility. Nothing else: no create, delete, enrol, grade, role, file, H5P or quiz writes, and no way to move, add or remove a section or activity. The development fixture CLI also writes, but is not reachable from any MCP client or web service (see docs/SECURITY.md).
  • No arbitrary external-function execution. Functions are discoverable and describable; none can be invoked by name.
  • Confirmation tokens are signed with a per-process key, so they do not survive an MCP server restart. Plan again after a restart — and note that a retry through the tool is then impossible, since a retry must present the same plan.
  • Idempotency guarantees at-most-once per (user, key), not exactly-once.
  • A request's identity includes the plan it was made from, so reusing a key across two plans is a conflict even when the requested content is identical.
  • Each write locks its own target row - course, course_sections or course_modules - before re-reading and comparing state, so an ordinary Moodle edit cannot slip between the check and the write. There is deliberately no lock-any-table helper. Row locking is implemented for PostgreSQL, the MySQL family and SQL Server; only PostgreSQL is covered by tests.
  • A retry reports recorded fingerprints and the current state separately; the original content is not stored, so it cannot be reported.
  • stdio only. HTTP transport is a future addition; the domain layer is already transport-free.
  • No Course Blueprint, no diff/apply engine, no content generation.
  • No browser automation, screenshots or accessibility auditing.
  • moodle_course_inspect returns course structure, not learner performance: no grades and no per-user completion state.
  • Moodle's front page is a course row but not a teaching course, so moodle_course_inspect rejects it. moodle_course_list still reports it, flagged isSiteCourse.
  • H5P schema generation is one level deep: a nested library field fixes the wrapper shape and the allowed library versions, but its params follow that library's own semantics — fetch them with a second moodle_h5p_schema call.
  • Some H5P and Moodle constructs cannot be expressed in JSON Schema (showWhen conditions, HTML tag whitelists, PCRE patterns, PARAM cleaning rules). They are preserved as x-h5p-* / x-moodle-* annotations and reported as conversion notes rather than dropped.
  • Moodle REST cannot express an empty array or a true null; the client reports both as explicit warnings to the server log — they are not returned to the MCP client.
  • The plugin is bind-mounted into the container from this repository; the rsync copy is kept only as a fallback. A host symlink does not work, for reasons explained in docs/LOCAL-DEV.md.

Licensing

  • apps/mcp-server/ — MIT
  • moodle/local/aimcp/ — GPL-3.0-or-later (required: it is a Moodle plugin)

No GPL implementation code is copied into the MIT server. Reference projects were studied as architecture references and reimplemented clean-room; the reasoning, per project, is in docs/REFERENCE-ARCHITECTURE.md.

Documentation

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured