Getting Started
moodle-mcp connects your AI assistant to your Moodle. Set it up once and you can ask things like “what’s due this week?” or “read the HW1 brief, work through the problems, and submit a draft” — and your assistant does the clicking through Moodle for you.
This guide takes about five minutes and needs no programming. By the end, your assistant will be reading your real courses, deadlines, files, and forums inside a normal chat.
Prerequisites
- A Moodle account on a site running Moodle 3.7 → 5.x — the same site you already open in the browser
- An AI assistant that supports MCP — Claude Code, Codex, OpenCode, pi, or any other MCP client
- Five minutes
Note: moodle-mcp connects through the same web-service channel as Moodle’s official mobile app, so most school sites already allow it. If the login in step 2 fails, your site has it switched off — see Security & Authentication for what to ask your administrator.
What You’ll Set Up
One small helper program sits between your assistant and your school’s Moodle:
flowchart LR
you["You"] -->|"chat"| ai["AI assistant<br/>Claude Code · Codex · OpenCode · pi"]
ai -->|"via moodle-mcp"| moodle["Your Moodle<br/>courses · deadlines · files"]
moodle-mcp teaches your assistant 20 ready-made Moodle actions: check deadlines, download assignment PDFs, upload and submit drafts, catch up on forums, read grades, and more. You stay in control — anything that could change your Moodle asks you first, and submissions stay drafts until you say otherwise.
Step 1: Install moodle-mcp
Download a ready-made program for your platform from the
latest release and
put it somewhere on your PATH — or, if you have Rust installed:
cargo install moodle-mcp
Every install option is on the Installation page.
Step 2: Connect It to Your Moodle (One-Time Login)
Moodle apps don’t send your password with every request — they log in once
and receive a personal key (a token), the same way Moodle’s mobile app
works. The included auth command does this login for you:
moodle-mcp auth
# Moodle URL (e.g. https://moodle.example.com): https://moodle.example.com
# Username: yourname
# Password: ********
# → export MOODLE_URL='https://moodle.example.com' MOODLE_TOKEN='…'
Keep the two values it prints (MOODLE_URL and MOODLE_TOKEN) — you’ll
paste them in the next step.
Tip: Schools with a maintained SSO driver (see Security & Authentication) get their portal replayed by
authwith the same credentials — no separate Moodle password needed. Every other school uses the credentials mode; if that answersinvalidlogin, ask your Moodle admin for a token.
Step 3: Add It to Your Assistant
Tell your assistant where the program lives and which Moodle to use. For Claude Code, one command does it:
claude mcp add moodle \
--env MOODLE_URL=https://moodle.example.com \
--env MOODLE_TOKEN=your-token \
-- /path/to/moodle-mcp
Or add it to your project’s .mcp.json:
{
"mcpServers": {
"moodle": {
"command": "/path/to/moodle-mcp",
"env": {
"MOODLE_URL": "https://moodle.example.com",
"MOODLE_TOKEN": "your-token"
}
}
}
}
Use the full path to the program from step 1, and paste the URL and token from step 2.
Any other MCP client takes the same two values in its own config format:
Codex uses ~/.codex/config.toml, OpenCode uses opencode.json, and pi
uses pi mcp add or .pi/mcp.json. Ready-made snippets for each are on
the Configuration page.
Warn: The token is your personal key — treat it like a password and never commit it to a shared repository.
Then restart your assistant so it picks up the new connection.
Step 4: Try It
Open a chat with your assistant and ask:
What's due in the next two weeks? Plan my studying around it.
Finish my Information Security HW1: read the assignment brief and the
attached PDF, work through the problems, and submit a draft.
I missed what happened in Data Communication this week — catch me up,
including the announcements forum.
When the answers mention your real courses and dates, you’re done. The assistant chains the actions by itself; see Tools for the full list.
If Something Goes Wrong
- Your assistant starts without Moodle tools — restart it after editing the config, and check that the config file is the one your assistant actually reads.
moodle-mcp: command not found— the assistant can’t find the program; put its full path in the config.- Login in step 2 fails — your site has web services switched off, or your account signs in through single sign-on and has no Moodle password; ask your administrator — details in Security & Authentication.
- The assistant reports “unauthorized” — the token is wrong or was
revoked. Run
moodle-mcp authagain and update the config.
What’s Next
- Tools — everything your assistant can now do in Moodle
- Configuration — timeouts, read-only mode, and other options
- Security & Authentication — how the token is protected
- Changelog — release notes and how to update
Installation
Prebuilt Binaries
No Rust toolchain needed — grab the archive for your platform from the latest release:
| Platform | Archive |
|---|---|
| Linux x86_64 | moodle-mcp-<version>-x86_64-unknown-linux-gnu.tar.gz |
| Linux arm64 | moodle-mcp-<version>-aarch64-unknown-linux-gnu.tar.gz |
| macOS Apple Silicon | moodle-mcp-<version>-aarch64-apple-darwin.tar.gz |
| macOS Intel | moodle-mcp-<version>-x86_64-apple-darwin.tar.gz |
| Windows x64 | moodle-mcp-<version>-x86_64-pc-windows-msvc.zip |
tar xzf moodle-mcp-*-aarch64-apple-darwin.tar.gz
sudo mv moodle-mcp-*/moodle-mcp /usr/local/bin/ # or anywhere on PATH
# Windows (PowerShell): unpack, then add the folder to your PATH
Expand-Archive moodle-mcp-<version>-x86_64-pc-windows-msvc.zip
Each archive is around 9 MB and unpacks to a single self-contained binary (about 22 MB) plus the README and license — there are no runtime dependencies.
Tip: If macOS refuses to run a downloaded binary (“cannot be verified”), clear the quarantine flag once:
xattr -d com.apple.quarantine /usr/local/bin/moodle-mcp.
Cargo
cargo install moodle-mcp # needs Rust 1.88+
From Source
git clone https://github.com/G36maid/moodle-mcp
cd moodle-mcp
cargo build --release
# binary: target/release/moodle-mcp
Verify the Install
moodle-mcp with no arguments starts the MCP stdio server — it blocks
waiting for a client, which is expected. To confirm the binary is on your
PATH and see its version, run it with an unknown subcommand:
moodle-mcp help
# moodle-mcp: unknown subcommand 'help'
# usage: moodle-mcp [serve] | moodle-mcp auth [--url <site> ...]
When the server starts it logs starting moodle-mcp v0.1.4 for … to stderr
(logs go to stderr; stdout is reserved for the MCP protocol stream).
Next Steps
Continue with Getting Started to get a token and connect your client. Release notes live on the Changelog page.
Changelog
Every release gets a section in the repository’s
CHANGELOG.md
(Keep a Changelog format, semantic
versioning). Pushing a v* tag builds the prebuilt binaries for every
platform and publishes the crate to crates.io, so a release is one tag
from start to finish.
This page summarizes the highlights of past releases; the changelog in the repository is the complete record.
0.2.0 — 2026-10-19
- per-school SSO drivers:
authlogs into SSO-federated schools by replaying the school’s own login chain over plain HTTP — no browser, no token paste (verified live against NTUST’s IdentityServer portal); adding a school is one file and one PR undersrc/sso/ - the escape-hatch pair
call_ws_function+list_ws_functionsis now opt-in viaMOODLE_ENABLE_WS_FUNCTION=true;list_functionswas renamed tolist_ws_functions - https is enforced end to end: schemeless URLs default to https, plain
http is rejected (loopback exempt,
MOODLE_ALLOW_HTTPopts out), and redirects from the site never drop to plain http - safety-relevant env values fail loudly on typos instead of silently
defaulting — booleans and
MOODLE_TIMEOUT_SECS - token acquisition failures name the fix for every
login/token.phperror code, andauthfollows schools that migrated to a new host - a landing page and this documentation book, published at https://moodle.ntnu.cc
- security-review hardening: quote-safe
authoutput, an https guard on the SSO callback, fully in-memory document conversion (no temp files)
0.1.4 — 2026-10-15
get_course_overviewincludes the content teachers put in section summaries and module descriptions (#13)- section and module availability restrictions and hidden states are surfaced — a restricted item is no longer listed as freely available
0.1.3 — 2026-10-14
download_fileno longer creates missing parent directories; it fails with an error naming the missing directory and suggesting the fix (#1)
0.1.2 — 2026-10-14
- published on crates.io —
cargo install moodle-mcp MOODLE_READ_ONLYandMOODLE_DISABLE_TOOLSfor cautious setups (#10)MOODLE_TIMEZONEand timezone detection fixes (#8)- a dozen fixes across downloads, uploads, notifications, grades, user search, and markdown rendering (#2, #3, #4, #5, #6, #7, #9, #12)
0.1.1 — 2026-10-13
- interactive
moodle-mcp authwith masked password prompts; flag variants for scripting - README translations (zh-TW, zh-CN, ja, es, de, fr, pt-BR)
0.1.0 — 2026-10-13
- initial release: 22 tools covering courses, assignments, forums, files,
messaging, notifications, and users, plus a
call_ws_functionescape hatch; document conversion built in; verified against Moodle 3.7 and designed for 4.x/5.x
Updating
- Prebuilt binary — download the latest archive from the releases page and replace the binary (same steps as Installation)
- Cargo — re-running the install command replaces the old binary:
cargo install moodle-mcp - From source —
git pullthencargo build --release
The server logs its version at startup
(starting moodle-mcp v0.1.4 for …); a cargo install can be checked
without starting it via cargo install --list | grep moodle-mcp.
Configuration
moodle-mcp is configured entirely through environment variables set on the server process — there is no config file. Every MCP client lets you set environment variables in its server definition.
Required Variables
| Variable | Description |
|---|---|
MOODLE_URL | Site base URL, no trailing slash |
MOODLE_TOKEN | Web service token (see Security & Authentication) |
All Variables
| Variable | Required | Default | Description |
|---|---|---|---|
MOODLE_URL | ✔ | — | Site base URL, no trailing slash |
MOODLE_TOKEN | ✔ | — | Web service token |
MOODLE_TIMEOUT_SECS | — | 60 | Per-request timeout; also bounds the startup timezone lookup |
MOODLE_TIMEZONE | — | auto / UTC | IANA name (Asia/Taipei) for rendering timestamps. The default detects the zone Moodle uses for you and falls back to UTC |
MOODLE_ALLOW_EXTERNAL_FETCH | — | off | Lets fetch_file/download_file fetch URLs outside the configured site (off: only same-site Moodle file URLs) |
MOODLE_ALLOW_HTTP | — | off | true allows a plain-http MOODLE_URL — otherwise http is rejected to protect the token. Loopback hosts are always exempt |
MOODLE_READ_ONLY | — | off | true hides all write/upload tools from the tool list — read-only LLM access |
MOODLE_ENABLE_WS_FUNCTION | — | off | true exposes the call_ws_function escape hatch — any web-service function your token allows, including writes |
MOODLE_DISABLE_TOOLS | — | — | Comma-separated tool names to hide on top (e.g. send_message,upload_file); unknown names warn at startup |
MOODLE_LOG_LEVEL | — | info | Log verbosity: error/warn/info/debug/trace. trace logs response-body snippets — avoid on shared machines |
MOODLE_UA | — | moodle-mcp/{version} | User-Agent override for institutional-filtering edge cases |
RUST_LOG | — | info | Advanced log filter override (logs go to stderr; stdout is the MCP stream) |
Boolean switches accept true/false — also 1/0, yes/no,
on/off, case-insensitive. Any other value fails at startup with the
correct spelling, so a typo can never silently disable a safety switch.
Read-Only Mode
Set MOODLE_READ_ONLY=true to hide every write and upload tool
(create_forum_discussion, reply_to_discussion, submit_assignment,
send_message, upload_file, call_ws_function). The agent can still read
everything your token can see, but nothing you say can make it post.
To hide individual tools while keeping the rest writable, list them in
MOODLE_DISABLE_TOOLS:
MOODLE_DISABLE_TOOLS=send_message,upload_file
Tip: For a shared or classroom machine,
MOODLE_READ_ONLY=trueplusMOODLE_LOG_LEVEL=warnkeeps the agent useful and the logs quiet.
The Escape Hatch
call_ws_function can invoke any of the ~350 web-service functions your
token allows — including writes — so the escape hatch pair is not on the
default tool list. Opt in when the 20 curated tools are not enough:
MOODLE_ENABLE_WS_FUNCTION=true
Every call still passes through your client’s confirmation for destructive
tools, and MOODLE_READ_ONLY hides call_ws_function again regardless of
this setting. The opt-in covers list_ws_functions too — a discovery tool
whose calls cannot be executed would just dead-end the agent — but being
read-only, it survives when MOODLE_READ_ONLY and this setting are both
on.
Timeouts and Timezones
MOODLE_TIMEOUT_SECS bounds each API request and the startup timezone
lookup. A value that is not a number of seconds (or is 0) fails at
startup — the same rule the boolean switches follow, so a typo can never
silently shorten every timeout to nothing. File downloads stream to disk
and use a read timeout instead of a total timeout, so large files finish
while stalled connections still die.
By default timestamps render in the timezone Moodle itself uses for you:
the site’s forced timezone, then your profile timezone if the site shows
it, then the site default (the site-level values need Moodle 4.2+), and
UTC otherwise. Set MOODLE_TIMEZONE to an IANA name to override
detection.
Example: Read-Only Server for Claude Code
{
"mcpServers": {
"moodle": {
"command": "/path/to/moodle-mcp",
"env": {
"MOODLE_URL": "https://moodle.example.com",
"MOODLE_TOKEN": "your-token",
"MOODLE_READ_ONLY": "true"
}
}
}
}
Example: Codex
~/.codex/config.toml — one file shared by the Codex CLI, the ChatGPT
desktop app, and the IDE extension — or .codex/config.toml for a single
trusted project:
[mcp_servers.moodle]
command = "/path/to/moodle-mcp"
env = { "MOODLE_URL" = "https://moodle.example.com", "MOODLE_TOKEN" = "your-token" }
Example: OpenCode
opencode.json (project) or ~/.config/opencode/opencode.json (global):
{
"mcp": {
"moodle": {
"type": "local",
"command": ["/path/to/moodle-mcp"],
"environment": {
"MOODLE_URL": "https://moodle.example.com",
"MOODLE_TOKEN": "your-token"
}
}
}
}
Example: pi
.pi/mcp.json (project) or ~/.pi/agent/mcp.json (global):
{
"mcpServers": {
"moodle": {
"command": "/path/to/moodle-mcp",
"env": {
"MOODLE_URL": "https://moodle.example.com",
"MOODLE_TOKEN": "your-token"
}
}
}
}
Or register it from the terminal:
pi mcp add moodle \
--env MOODLE_URL=https://moodle.example.com \
--env MOODLE_TOKEN=your-token \
-- /path/to/moodle-mcp
The command writes the user-level config by default; add -l to write the
project file instead. Run /reload inside pi after editing either file
outside a session.
Note:
.pi/mcp.jsonis gitignored by convention because it holds a real token — never commit tokens.
Tools
moodle-mcp exposes 22 tools: 20 curated ones that mirror how you navigate
Moodle by hand are always available, and a two-tool escape hatch —
list_ws_functions + call_ws_function — reaches everything else your
token allows once you enable it with MOODLE_ENABLE_WS_FUNCTION=true.
There is no 1:1 dump of the ~350 raw Moodle web-service functions — the
curated set covers the student journey; the escape hatch covers the rest.
Every tool returns structured JSON in structuredContent. Document tools
(get_course_overview, get_assignment) additionally return markdown in
text. Nulls are stripped recursively, because Moodle 3.7 omits many
fields that 4.x/5.x include.
Read vs. Write
Each tool is either read-only or a write. Write tools act as the token owner — the server’s instructions tell the agent to confirm with you before calling them, and the tool descriptions repeat it. Uploads land in your private draft area, not with your teacher, until you explicitly finalize a submission.
Identity & Courses
| Tool | Signature | Read/Write | Purpose |
|---|---|---|---|
whoami | () | read | Token owner identity + site/release |
list_courses | (search?) | read | Enrolled courses; optional name filter |
get_course_overview | (course_id) | read | Whole-course markdown map |
get_course_overview merges five Moodle endpoints into a single markdown
document: sections, files, assignments, quizzes, forums, and grades — every
ID and file URL embedded, so one call gives the agent (and you) the whole
course map.
Assignments
| Tool | Signature | Read/Write | Purpose |
|---|---|---|---|
get_assignment | (assignment_id) | read | Full document: times, state, description, attachments, submission, grade |
submit_assignment | (assignment_id, text?, item_id?, finalize?) | write | The single write: online text and/or draft files, optionally finalizing |
Forums
| Tool | Signature | Read/Write | Purpose |
|---|---|---|---|
get_forum_discussions | (forum_id) | read | Thread list with opening-post previews |
get_discussion_posts | (discussion_id) | read | Whole thread as markdown, attachments included |
create_forum_discussion | (forum_id, subject, message) | write | Start a thread |
reply_to_discussion | (post_id, subject, message) | write | Reply (to the opening post = reply to the thread) |
Files
| Tool | Signature | Read/Write | Purpose |
|---|---|---|---|
fetch_file | (url) | read | PDF/DOCX/PPTX/XLSX/ODT/RTF/EPUB/CSV → markdown; 100k-character cap |
download_file | (url, save_to) | read | Raw bytes → local path (metadata only) |
upload_file | (path, item_id?) | write | Local file → draft area |
File tools take and return local file paths, never base64. Same-site
URLs only, unless MOODLE_ALLOW_EXTERNAL_FETCH=1.
Grades, Schedule & Messaging
| Tool | Signature | Read/Write | Purpose |
|---|---|---|---|
get_grades | (course_id?) | read | Cross-course summary, or every grade item of one |
get_schedule | (time_from?, time_to?, course_ids?) | read | Merged timeline (default: next 30 days) |
get_notifications | (read?, limit?) | read | Recent notifications |
get_conversations | (limit?) | read | Conversations with member ids |
send_message | (user_id, text) | write | Direct message |
get_quiz_attempts | (quiz_id) | read | Attempt records (read-only) |
get_user | (field, value) | read | Profile lookup |
search_users | (search?, limit?) | read | User search |
Escape Hatch
| Tool | Signature | Read/Write | Purpose |
|---|---|---|---|
call_ws_function | (wsfunction, params?) | read/write | Call any enabled web-service function (PHP bracket notation handled) |
list_ws_functions | (query?) | read | Discover callable web-service functions by keyword |
call_ws_function is an intentional overwriting surface. It is hidden
unless MOODLE_ENABLE_WS_FUNCTION=true opts in, and MOODLE_READ_ONLY
hides it again regardless.
ID Flow
Tools chain through IDs: each level’s output embeds the IDs for the next.
| ID | Produced by | Consumed by |
|---|---|---|
course_id | list_courses, get_course_overview | overview, grades, schedule, assignments |
forum_id | get_course_overview | get_forum_discussions, create_forum_discussion |
discussion_id | get_forum_discussions | get_discussion_posts |
post_id | get_discussion_posts | reply_to_discussion |
assignment_id | get_course_overview, get_assignment | get_assignment, submit_assignment |
item_id | upload_file | submit_assignment |
user_id | conversations, notifications, get_user | send_message, get_user |
What Is Intentionally Not Provided
Quiz-taking. get_quiz_attempts reads attempt records, but nothing answers
questions for you. This is a design decision, not a gap.
Version Differences
moodle-mcp targets Moodle 3.7 → 5.x. Differences between versions (function names, pagination, response shapes) are absorbed by the server, not by your prompts — the same tool call works across all supported versions.
Security & Authentication
Get a Token
moodle-mcp auth obtains a Moodle web service token without touching
curl. It has two modes:
Credentials mode (default). Prompts for site, username, and password
(masked as you type), then POSTs to /login/token.php — the same flow the
official Moodle mobile app uses. Flags (--url, --username, --password)
skip the matching prompts, so scripts work non-interactively:
moodle-mcp auth
moodle-mcp auth --url https://moodle.example.com --username yourname
SSO-federated schools. auth dispatches by host: a school with a
maintained driver under the project’s src/sso/ gets its own login
portal replayed over plain HTTP with the same credentials (no browser;
the SSO password travels only to the school’s portal). Schools without
a driver use the credentials mode — if that answers invalidlogin, ask
your Moodle admin for a token (Site administration → Server → Web
services → Manage tokens) or contribute a driver (one file, one PR).
Other flags: --service names the web service (default
moodle_mobile_app), --output json prints machine-readable output, and
--allow-http permits a plain-http site URL.
If the token request fails, the site is missing the REST protocol or the
moodle_mobile_app service for your account — ask your administrator.
What the Server Does With the Token
The token travels with every request, so the server treats it as a live credential:
- Same-site injection only. The token is injected into
pluginfile.phpURLs; redirects that leave the Moodle host are stopped. Fetching an external URL with your token is not possible unless you setMOODLE_ALLOW_EXTERNAL_FETCH=1. - Redaction. The token never appears in tool output or errors — any
token=…in a rendered error message becomestoken=[REDACTED]. - No stdout leakage. Logs go to stderr; stdout is the MCP protocol stream, so the token never mixes into it.
Transport Security
Use HTTPS. A plain-http MOODLE_URL is rejected because the token
(and, on auth, the password) would travel in cleartext. Two exemptions:
- Loopback hosts (
localhost,127.0.0.1) are always allowed, for local test instances. MOODLE_ALLOW_HTTP=1(orauth --allow-http) explicitly opts out, for isolated networks where you accept the risk.
Write Safety
Write tools act as the token owner. Three layers keep that deliberate:
- The server’s instructions and each write tool’s description tell the agent to confirm with you before calling.
- Submissions prefer drafts:
submit_assignmentfinalizes only when you ask; uploads go to your private draft area until then. MOODLE_READ_ONLY=trueremoves the write tools from the tool list entirely — see Configuration.
Quiz-taking is intentionally not provided.
Transfer Limits
| Direction | Limit | Notes |
|---|---|---|
| Inline fetch | 100 MB | fetch_file, capped at 100k characters after conversion |
| Streamed download | 500 MB | download_file streams to disk, never buffered whole |
| Upload | 100 MB | upload_file |
Downloads use a read timeout rather than a total timeout, so large files finish while stalled connections still die.
Token Hygiene
-
Never commit the token. Keep it in the client’s env block, and keep token-bearing config files out of version control. This repository gitignores every client’s project config for exactly that reason:
- Claude Code —
.mcp.json(matched by the*mcp.jsonrule) - Codex —
.codex/(the user-level~/.codex/config.tomlalready lives outside any repository) - OpenCode —
opencode.json/opencode.jsonc - pi —
.pi/mcp.json
In other repositories, prefer each client’s user-level (global) config for token-bearing servers, or ignore the project config the same way.
- Claude Code —
-
The token carries your full student permissions. Use
MOODLE_READ_ONLY=truewhen the agent does not need to write. -
To revoke a token, log in to Moodle and remove it on the Security keys page (
/user/managetoken.php). -
Avoid
MOODLE_LOG_LEVEL=traceon shared machines — it logs response-body snippets.
Testing Schools Responsibly
Verifying an SSO driver means logging into a school’s production portal with a real account — treat the school’s systems and reputation as part of the job:
- Authorized accounts only. Verify with an account you own or one you are explicitly permitted to test; school infrastructure remains the school’s property, and its acceptable-use policy applies.
- A handful of requests is the norm. A driver verification is a single-digit number of HTTP calls. Never scan, brute-force, stress, or probe beyond what the driver needs.
- Weaknesses you notice belong to the school. An unenforced
captcha, a broken certificate chain, an open redirect — report it
through the school’s own channel (the computer center) instead of
this repository. Keep such observations out of code, commit messages,
docs and issue trackers; private local notes in the gitignored
.secrets/directory are the place. The driver’s documentation states what works, never what is broken.
Development
moodle-mcp is a single Rust crate (library plus a thin binary) built with Rust 1.88 or later. The whole development loop runs offline: the test suite fakes the Moodle REST endpoint, so you never need a live Moodle site to hack on it.
Clone, build, and run the checks that CI runs:
git clone https://github.com/G36maid/moodle-mcp
cd moodle-mcp
cargo build
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test
Bird’s-eye view of the codebase
The server is small enough to read in one sitting — six modules carry everything:
src/main.rsis a thin binary: no arguments serves MCP over stdio, and everything else is delegated. Theauthsubcommand lives insrc/auth.rsand fetches a web-service token with interactive prompts;--url/--username/--passwordflags skip them for scripting.src/client.rsis the only place that talks to Moodle. Every call goes through one endpoint,POST {base}/webservice/rest/server.php, with parameters flattened to PHP bracket notation (courseids[0]=2). Tworeqwestclients exist by design: API calls carry a total timeout, while file downloads stream without one (a read timeout kills stalled connections). Redirects are host-locked because the token gets injected intopluginfile.phpURLs — don’t loosen that.src/tools/holds one module per domain (assignments.rs,forums.rs,files.rs, …). rmcp requires every#[tool]method to live in the single#[tool_router]impl block inmod.rs: declare the method there, put the business logic in the domain module.src/output.rsimplements the dual-channel result policy: every tool returns structured JSON instructuredContent, and document tools add a markdown rendering intext. Nulls are stripped recursively because Moodle 3.7 omits many fields. Only markdown is truncated (MAX_DOC_CHARS), never the JSON.src/sso/holds the per-school SSO drivers — one self-contained file per school whose login cannot go throughlogin/token.php(SSO portals: the password belongs to the identity provider). See the next section.tests/mirrors the suite CI runs:tests/client_integration.rsmocks the REST endpoint with wiremock, andtests/mcp_protocol.rsruns the real server over an in-memory duplex transport — protocol behavior is tested without a socket or a Moodle.
Adding a school’s SSO driver
src/sso/ exists because some schools federate login through a portal
(OIDC/SAML): the password belongs to the identity provider, so the
credentials mode answers invalidlogin no matter what. Each school gets
one self-contained file — src/sso/ntust.rs is the reference — holding
the metadata (host, Moodle version, identity provider, the date the flow
was verified live), the host match, and the whole replayed chain.
src/sso/mod.rs carries the shared launch-flow helpers (launch.php
URL building, moodlemobile:// payload decoding, token validation) and
the host-matched dispatch; auth consults it before any request moves,
so schools without a driver simply use the credentials mode.
Adding a school is one file plus one arm:
- Copy
src/sso/ntust.rsas the template and keep the metadata block honest — a flow verified live on a date, or not verified at all. - Adjust the replayed chain to the school’s portal. Every step’s error
message names what broke, and the password never crosses plain http
(the loopback carve-out matches
MOODLE_ALLOW_HTTP). - Add the
matches(host)arm toschool_forinsrc/sso/mod.rs. - Cover the whole chain with an offline wiremock test (see the one in
ntust.rs), then verify once against the live portal before opening the PR — responsibly: authorized accounts, a handful of requests, and weaknesses you notice go to the school, not to this repository (see Security & Authentication).
Never probe or guess: school_for is an exact host match, so an
unknown school fails with guidance instead of leaking credentials to
the wrong form. If a second school turns out to share the same flow
shape, extract the shared steps into a family module then — one member
is not a family.
Rules the codebase enforces
These come from the root AGENTS.md and are review-gated, not optional:
- Never write to stdout. stdout is the MCP JSON-RPC stream; logs go
to stderr via
tracing(RUST_LOGfor advanced filtering). - Write tools must say so. Their descriptions carry “WRITE operation — confirm with the user before calling”, and drafts are preferred over finalizing submissions. Quiz-taking is intentionally not provided — don’t add it.
- Annotations have a policy. Read tools set
read_only_hint = true; additive write tools setdestructive_hint = false; overwriting surfaces (submit_assignment,call_ws_function) keep the spec default. Preserve these when adding tools. - Tool descriptions are structured prose — purpose first, workflow and boundary sentences only where a real confusion pair exists, then upstream id chaining. No section labels, no examples.
- Comments explain why, never what — version quirks, trade-offs, upstream Moodle source links. No step-by-step stories.
Release process
- Add a section to
CHANGELOG.md— the release job extracts the tagged version’s section verbatim for the GitHub release notes and fails if it is missing. - Bump
versioninCargo.tomlto match the tag; a check in the release job enforces this. - Push the
v*tag: the workflow builds multi-platform binaries, creates the GitHub release from the changelog section, and publishes the crate to crates.io via trusted publishing.
What’s Next
- About — the project’s goals and design principles
- Tools — what the tools do and how ids flow
- Changelog — what each release changed
- GitHub issues — bug reports and feature ideas
About
What is moodle-mcp?
moodle-mcp is a Model Context Protocol server, written in Rust, that lets LLM agents use a real Moodle LMS as the token owner — the same way you use it: read courses, fetch assignment PDFs, work, upload, submit a draft. One static binary, no runtime dependencies.
What is MCP?
The Model Context Protocol (MCP) is an open standard that defines how LLM applications talk to external tools and data sources. An MCP client — Claude Code, Codex, OpenCode, pi, and other agent harnesses — connects to MCP servers, each exposing a set of typed tools the model can call. moodle-mcp is one such server: it speaks MCP over stdio and translates those tool calls into Moodle web-service requests.
What is Moodle?
Moodle is a free, open-source learning management system (LMS) used by universities, schools, and workplaces worldwide to run courses: assignment briefs and submissions, quizzes, forums, grades, and everything else around teaching. Your institution’s course website almost certainly runs it. moodle-mcp drives that same platform through its official web-service API.
The project
The project started from a simple observation: a chatbot with no way into your Moodle leaves every study task manual — five browser tabs to answer “what’s due this week?”, PDFs re-uploaded by hand into a chat window, forums and grades copy-pasted one page at a time. moodle-mcp gives the agent typed tools instead, and gets out of the way.
Design Principles
- Human-shaped tools. The tools mirror steps you’d actually take
clicking through Moodle, not a 1:1 dump of ~350 raw web-service
functions. An opt-in escape hatch (
call_ws_function) still reaches everything else. - Agent-native. Tools take and return local file paths instead of base64; results embed the IDs for the next tool; errors are plain-language and suggest the fix.
- Version-adaptive. Validated live against Moodle 3.7 → 5.x; response differences between versions are absorbed by the server.
- Safe by default. Write tools require explicit confirmation, drafts are preferred over irreversible submissions, plain HTTP is rejected, and quiz-taking is intentionally not provided.
Built With
| Crate | Role |
|---|---|
rmcp | Official Rust MCP SDK |
anydoc | Document conversion (PDF/DOCX/… → md) |
htmd | HTML → markdown conversion |
Links
- GitHub repository — source, issues, pull requests
- Releases — prebuilt binaries for Linux, macOS, and Windows
- crates.io —
cargo install moodle-mcp - CHANGELOG — release notes
License
MIT — see LICENSE.
Contributing
Bug reports and pull requests are welcome on GitHub. For development setup and architecture notes, start with AGENTS.md in the repository root; the test suite runs fully offline.