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