Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 auth with the same credentials — no separate Moodle password needed. Every other school uses the credentials mode; if that answers invalidlogin, 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 auth again and update the config.

What’s Next

Installation

Prebuilt Binaries

No Rust toolchain needed — grab the archive for your platform from the latest release:

PlatformArchive
Linux x86_64moodle-mcp-<version>-x86_64-unknown-linux-gnu.tar.gz
Linux arm64moodle-mcp-<version>-aarch64-unknown-linux-gnu.tar.gz
macOS Apple Siliconmoodle-mcp-<version>-aarch64-apple-darwin.tar.gz
macOS Intelmoodle-mcp-<version>-x86_64-apple-darwin.tar.gz
Windows x64moodle-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: auth logs 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 under src/sso/
  • the escape-hatch pair call_ws_function + list_ws_functions is now opt-in via MOODLE_ENABLE_WS_FUNCTION=true; list_functions was renamed to list_ws_functions
  • https is enforced end to end: schemeless URLs default to https, plain http is rejected (loopback exempt, MOODLE_ALLOW_HTTP opts 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.php error code, and auth follows 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 auth output, an https guard on the SSO callback, fully in-memory document conversion (no temp files)

0.1.4 — 2026-10-15

  • get_course_overview includes 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_file no 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_ONLY and MOODLE_DISABLE_TOOLS for cautious setups (#10)
  • MOODLE_TIMEZONE and 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 auth with 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_function escape 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 pull then cargo 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

VariableDescription
MOODLE_URLSite base URL, no trailing slash
MOODLE_TOKENWeb service token (see Security & Authentication)

All Variables

VariableRequiredDefaultDescription
MOODLE_URL✔—Site base URL, no trailing slash
MOODLE_TOKEN✔—Web service token
MOODLE_TIMEOUT_SECS—60Per-request timeout; also bounds the startup timezone lookup
MOODLE_TIMEZONE—auto / UTCIANA name (Asia/Taipei) for rendering timestamps. The default detects the zone Moodle uses for you and falls back to UTC
MOODLE_ALLOW_EXTERNAL_FETCH—offLets fetch_file/download_file fetch URLs outside the configured site (off: only same-site Moodle file URLs)
MOODLE_ALLOW_HTTP—offtrue allows a plain-http MOODLE_URL — otherwise http is rejected to protect the token. Loopback hosts are always exempt
MOODLE_READ_ONLY—offtrue hides all write/upload tools from the tool list — read-only LLM access
MOODLE_ENABLE_WS_FUNCTION—offtrue 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—infoLog 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—infoAdvanced 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=true plus MOODLE_LOG_LEVEL=warn keeps 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.json is 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

ToolSignatureRead/WritePurpose
whoami()readToken owner identity + site/release
list_courses(search?)readEnrolled courses; optional name filter
get_course_overview(course_id)readWhole-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

ToolSignatureRead/WritePurpose
get_assignment(assignment_id)readFull document: times, state, description, attachments, submission, grade
submit_assignment(assignment_id, text?, item_id?, finalize?)writeThe single write: online text and/or draft files, optionally finalizing

Forums

ToolSignatureRead/WritePurpose
get_forum_discussions(forum_id)readThread list with opening-post previews
get_discussion_posts(discussion_id)readWhole thread as markdown, attachments included
create_forum_discussion(forum_id, subject, message)writeStart a thread
reply_to_discussion(post_id, subject, message)writeReply (to the opening post = reply to the thread)

Files

ToolSignatureRead/WritePurpose
fetch_file(url)readPDF/DOCX/PPTX/XLSX/ODT/RTF/EPUB/CSV → markdown; 100k-character cap
download_file(url, save_to)readRaw bytes → local path (metadata only)
upload_file(path, item_id?)writeLocal 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

ToolSignatureRead/WritePurpose
get_grades(course_id?)readCross-course summary, or every grade item of one
get_schedule(time_from?, time_to?, course_ids?)readMerged timeline (default: next 30 days)
get_notifications(read?, limit?)readRecent notifications
get_conversations(limit?)readConversations with member ids
send_message(user_id, text)writeDirect message
get_quiz_attempts(quiz_id)readAttempt records (read-only)
get_user(field, value)readProfile lookup
search_users(search?, limit?)readUser search

Escape Hatch

ToolSignatureRead/WritePurpose
call_ws_function(wsfunction, params?)read/writeCall any enabled web-service function (PHP bracket notation handled)
list_ws_functions(query?)readDiscover 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.

IDProduced byConsumed by
course_idlist_courses, get_course_overviewoverview, grades, schedule, assignments
forum_idget_course_overviewget_forum_discussions, create_forum_discussion
discussion_idget_forum_discussionsget_discussion_posts
post_idget_discussion_postsreply_to_discussion
assignment_idget_course_overview, get_assignmentget_assignment, submit_assignment
item_idupload_filesubmit_assignment
user_idconversations, notifications, get_usersend_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.php URLs; redirects that leave the Moodle host are stopped. Fetching an external URL with your token is not possible unless you set MOODLE_ALLOW_EXTERNAL_FETCH=1.
  • Redaction. The token never appears in tool output or errors — any token=… in a rendered error message becomes token=[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 (or auth --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:

  1. The server’s instructions and each write tool’s description tell the agent to confirm with you before calling.
  2. Submissions prefer drafts: submit_assignment finalizes only when you ask; uploads go to your private draft area until then.
  3. MOODLE_READ_ONLY=true removes the write tools from the tool list entirely — see Configuration.

Quiz-taking is intentionally not provided.

Transfer Limits

DirectionLimitNotes
Inline fetch100 MBfetch_file, capped at 100k characters after conversion
Streamed download500 MBdownload_file streams to disk, never buffered whole
Upload100 MBupload_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.json rule)
    • Codex — .codex/ (the user-level ~/.codex/config.toml already 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.

  • The token carries your full student permissions. Use MOODLE_READ_ONLY=true when 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=trace on 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.rs is a thin binary: no arguments serves MCP over stdio, and everything else is delegated. The auth subcommand lives in src/auth.rs and fetches a web-service token with interactive prompts; --url/--username/--password flags skip them for scripting.
  • src/client.rs is 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). Two reqwest clients 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 into pluginfile.php URLs — 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 in mod.rs: declare the method there, put the business logic in the domain module.
  • src/output.rs implements the dual-channel result policy: every tool returns structured JSON in structuredContent, and document tools add a markdown rendering in text. 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 through login/token.php (SSO portals: the password belongs to the identity provider). See the next section.
  • tests/ mirrors the suite CI runs: tests/client_integration.rs mocks the REST endpoint with wiremock, and tests/mcp_protocol.rs runs 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:

  1. Copy src/sso/ntust.rs as the template and keep the metadata block honest — a flow verified live on a date, or not verified at all.
  2. 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).
  3. Add the matches(host) arm to school_for in src/sso/mod.rs.
  4. 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_LOG for 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 set destructive_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

  1. 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.
  2. Bump version in Cargo.toml to match the tag; a check in the release job enforces this.
  3. 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

CrateRole
rmcpOfficial Rust MCP SDK
anydocDocument conversion (PDF/DOCX/… → md)
htmdHTML → markdown conversion

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.