moodle-mcp
A Model Context Protocol server that gives AI assistants (Claude, opencode, any MCP client) safe access to Moodle, and syncs your courses, grades, deadlines and materials into an Obsidian vault.
Works with any Moodle that has Web Services and the mobile app service enabled — including universities behind federated SSO (Entra ID, SAML, Shibboleth, CAS), where students can't generate an API token by hand.
Why this one
Most MCP servers for third-party platforms pipe untrusted content straight into the model and expose every write action by default. Moodle is a worst case for that: any participant in a course can write forum posts the model will read, and actions like submitting an assignment can't be undone. This server is designed around that threat model:
- Read-only by default. Write tools (submissions, forum posts, messages) and the raw Web Services API are off unless you enable them explicitly. 18 of the 22 default tools carry
readOnlyHint, so a client can auto-approve them without auto-approving anything irreversible. - Prompt-injection defenses on the server side (OWASP LLM01), not delegated to the model:
- hidden content is stripped from every Moodle response —
display:none,font-size:0,hidden, HTML comments, Unicode tag characters, zero-width and bidi control characters — so the model reads what the student sees; - notes written to the vault can't execute anything:
dataviewjs/dataviewblocks, inline Dataview and Templater commands are neutralised, remote images become links (no tracking pixels); - every Moodle write requires user confirmation via MCP elicitation, showing the exact recipient and text — even when the client is set to auto-approve.
- hidden content is stripped from every Moodle response —
- Token confinement. Credentials never appear in tool responses. The token is only ever attached to
pluginfile.phpURLs on the configured instance; any other host is refused before a request is made. Tools that can overwrite credentials are off by default, so a malicious forum post can't swap your token. - Context-budget aware. The full tool set costs ~9,700 tokens per request; selectable toolsets cut that to ~1,750–5,500.
Features
- SSO token capture —
moodle-mcp --loginstarts the mobile-app login in your browser with a single-use passport; amoodlemobile://URL handler receives the redirect, checks Moodle's signature and stores the token (0600). Registered natively on Linux (xdg-mime), Windows (HKEY_CURRENT_USER, no admin) and macOS (an AppleScript applet registered with Launch Services). The token never passes through the model, and the handler moves the URL out of its command-line arguments at once, so other local users can't read it withps. - Obsidian dashboard sync — a central note with every course, progress, current grades and a sorted deadline calendar; one note per course with grade report, per-assignment submission status, assignment briefs converted to Markdown and the resource tree.
- What changed — each sync compares with the previous one and writes a note (and
get_recent_changesreturns it, without calling Moodle): new grades, changed deadlines, new assignments and quizzes, new materials, new forum topics and topics with new replies. - Deadlines everywhere — a
.icscalendar next to the dashboard (import or subscribe in Google Calendar, Outlook, Thunderbird or your phone; a reminder the day before), and an Obsidian Tasks list (- [ ] … 📅 YYYY-MM-DD) in the dashboard. - Scheduled sync —
moodle-mcp --schedule 6syncs every 6 hours with the OS scheduler (systemd timer, launchd or Windows Task Scheduler), no MCP client running.moodle-mcp --syncruns one sync now. - English or Portuguese notes —
moodle-mcp --set language=enswitches the text of every generated note; folders stay put. - MCP resources and prompts —
moodle://changes,moodle://deadlinesandmoodle://dashboardserve the last sync without calling Moodle; theweekly_review,prepare_assignmentandforum_catch_upprompts are ready-made flows. The Vault tools return typed structured output (outputSchema). - Course material download — PDFs, slides and HTML pages saved into the vault, with each PDF converted to a searchable
.pdf.mdthat agents can read (optional[pdf]extra). - 33 tools across courses, books and pages, completion, grades, assignments and submissions, quizzes, forums (search, export to Obsidian, reply), messages and calendar.
- Multi-instance — an open registry of Moodle instances; when more than one is configured, the
instanceparameter is exposed as an enum in the schema, so the model gets the valid names instead of free text.
Install
The package is on PyPI as moodle-obsidian-mcp; it installs two commands, moodle-mcp and moodle-mcp-token-catcher.
uv tool install "moodle-obsidian-mcp[pdf]" # or: pipx install "moodle-obsidian-mcp[pdf]"
moodle-mcp --install-handler # once: register the moodlemobile:// handler
moodle-mcp --login # log in through your browser (SSO works)
Upgrade with uv tool upgrade moodle-obsidian-mcp (or pipx upgrade moodle-obsidian-mcp).
Use a permanent install (uv tool or pipx) rather than uvx: the login handler is registered with the executable's path, and uvx runs from a cache that can be wiped. To try it without installing:
uvx --from moodle-obsidian-mcp moodle-mcp --show-config
From a clone, ./install.sh [vault-path] does the permanent install, sets the vault, registers the handler and tells you to run moodle-mcp --login. For development, see Development.
The [pdf] extra adds PDF text extraction via PyMuPDF. It's kept out of the base install because PyMuPDF is AGPL-3.0; without it everything works, PDF downloads just don't get the companion .md.
moodle-mcp --uninstall-handler removes the moodlemobile:// handler again.
Reliability
- Your notes are yours. Generated content lives between
%% moodle-mcp:start %%and%% moodle-mcp:end %%; anything you write outside that block, and any frontmatter properties you add, survive every sync. A note from an older version (without the markers) is copied to.moodle-mcp/backup/in the vault before its first rewrite. - Incremental downloads. Each file's
timemodifiedand size are recorded in.moodle-mcp/state-<instance>.jsonin the vault; files that haven't changed in Moodle and are still in the vault aren't downloaded again. - One failing course doesn't stop a sync. Each course syncs independently; a section Moodle refuses (quizzes, choices, grades) is left empty and reported in
errorswithstatus: "partial". Only a missing or invalid token stops everything, with instructions to log in again. - Moodle errors arrive as errors (
isErrorin MCP), with the cause and what to do, instead of being disguised as results. - Nothing is overwritten by accident: files, pages, forums and courses with the same name get
(2),(3)…; filenames are valid on Windows, macOS and Linux. - Transient failures are retried: reads back off on 429/5xx and connection errors (honouring
Retry-After); writes are never retried once the server has received them, so nothing gets posted twice. Connections are reused (keep-alive). - The server doesn't block: sync, material download and forum export run in a thread and report per-course progress to the client.
- Vault layout:
MOODLE_MCP_SYNC_SUBDIRputs the dashboard and course notes in a subfolder (links follow);MOODLE_MCP_DASHBOARD_NAMErenames the dashboard. With several instances, each gets its own dashboard. - Atomic, bounded downloads: a failed download leaves the previous file intact, and no file exceeds
MOODLE_MCP_MAX_DOWNLOAD_MB(500). - Audit trail: every action on Moodle is recorded in
audit.log(0600, in the config directory) with its outcome — confirmed, refused, or run without confirmation — with no tokens and rotation at 1 MB. - Less personal data:
get_course_participantsreturns only id, name, roles and groups; classmates' emails and profile data never reach the LLM provider. - Secrets:
credentials.jsonandlast_auth.logare written atomically and already0600; an unreadablecredentials.jsonis set aside rather than deleted;moodle-mcp --show-config(and startup) warn about files readable by other users.
Configuration
Nothing is required — every option has a default. Configuration is layered, highest precedence first:
- Environment variables — how MCP clients inject options (
envin Claude,environmentin opencode). config.tomlin the user config directory.credentials.jsonin the same directory — tokens only,0600..envnext to the code — legacy, still read.
The config directory follows the OS: ~/.config/moodle-mcp (Linux), ~/Library/Application Support/moodle-mcp (macOS), %LOCALAPPDATA%\moodle-mcp (Windows). It doesn't live next to the code because under uvx/pipx the package runs from an ephemeral, possibly read-only cache.
moodle-mcp --show-config # where everything is read from, secrets redacted
moodle-mcp --list-toolsets # which tools each toolset exposes
moodle-mcp --set sync_subdir=Moodle --set instances.utad.url=https://e-learning.utad.pt
moodle-mcp --set language=en # note text in English (default: pt)
Note options: language (pt/en), dashboard_name (default: an existing Doutoramento - Painel Moodle note, else Painel Moodle/Moodle Dashboard), courses_dir (default Unidades Curriculares), sync_subdir.
Scheduled sync
moodle-mcp --sync # sync every instance with a token, now
moodle-mcp --schedule 6 # every 6 hours (1–24); add --materials to download files too
moodle-mcp --unschedule
The task runs moodle-mcp --sync and reads the same config.toml and credentials.json; no token goes into the task files or the command line. The scheduler doesn't see your shell or MCP client environment, so put the vault in config.toml (moodle-mcp --set obsidian_vault_dir=…) rather than only in the client's env. The result of the last run is in last_sync.log, in the config directory.
Moodle instances
default_instance = "uab"
[instances.uab]
url = "https://elearning.uab.pt"
[instances.utad]
url = "https://e-learning.utad.pt"
or via environment: MOODLE_<NAME>_URL and MOODLE_<NAME>_TOKEN.
Unnamed MOODLE_URL / MOODLE_TOKEN still work as the default instance, but only while no named instance exists — otherwise one university's token could be sent to another's server.
Toolsets
--tools |
Tools | ~tokens | Writes to Moodle |
|---|---|---|---|
| (default) | 22 | ~5,500 | no |
core,grades,forums,obsidian,write |
29 | ~7,900 | yes |
all |
33 | ~9,700 | yes, plus the raw API |
core,obsidian |
13 | ~3,700 | no |
core |
8 | ~1,750 | no |
core is always included. write and raw log a warning at startup. auth (tools that store credentials) is off by default; even when on, set_moodle_token validates the token, accepts only https:// URLs and won't change an existing instance's URL.
Write confirmation
MOODLE_MCP_CONFIRM_WRITES:
| Value | When confirmation is requested |
|---|---|
always (default) |
before any Moodle action |
untrusted |
only after the session has read third-party content (posts, pages, messages) |
client |
never; relies on the MCP client's approval alone (for clients without elicitation — not recommended) |
Limits: a client that answers confirmation prompts automatically defeats the last line of defense, and visible malicious text still reaches the model. That's why write tools stay off by default.
Client setup
If the client doesn't inherit your shell's PATH, use the absolute path of moodle-mcp as the command (~/.local/bin/moodle-mcp with uv tool or pipx on Linux and macOS; see uv tool dir --bin).
Claude Code / Claude Desktop (mcpServers):
{
"mcpServers": {
"moodle": {
"command": "moodle-mcp",
"args": ["--tools", "core,obsidian"],
"env": { "MOODLE_URL": "https://your-moodle.example.edu" }
}
}
}
opencode (opencode.json):
{
"mcp": {
"moodle": {
"type": "local",
"command": ["moodle-mcp", "--tools", "core,obsidian"],
"environment": { "MOODLE_URL": "https://your-moodle.example.edu" },
"enabled": true
}
}
}
Browser login (SSO)
- Log in to your university's Moodle in the browser as usual.
- Run:
moodle-mcp --login # or: moodle-mcp --login utad
This opens a login link with a random, single-use passport valid for 15 minutes (--no-browserjust prints it). - Accept the "Open Moodle Token Catcher" prompt — the token is verified, stored in
credentials.jsonunder the right instance and the vault syncs immediately. The outcome is written tolast_auth.login the config directory.
The handler only accepts the response to a login started this way: Moodle signs it with md5(wwwroot . passport) and the handler checks that signature. A fixed passport would let any web page replace your token with another account's.
Note: this uses the same login flow as the official Moodle mobile app and only grants what your own account can already do. Check your institution's acceptable-use policy before automating access to its platform.
Development
git clone https://github.com/CarlosAndreFerreira/moodle-obsidian-mcp.git
cd moodle-obsidian-mcp
uv pip install -e ".[dev,pdf]"
ruff check moodle_mcp tests && ruff format --check moodle_mcp tests
mypy moodle_mcp
pytest
The test suite covers token safety (credential confinement, cross-host refusal), prompt-injection sanitisation, resilience and robustness, plus Hypothesis property tests for file names, URL validation and content sanitisation. The base modules (config, confirm, login, sanitize, transport, vault) are type-checked with strict mypy flags. CI runs lint, mypy, uv lock --check and tests on Linux, Windows and macOS (Python 3.11–3.13), plus pip-audit.
Releases are published to PyPI from a vX.Y.Z tag via trusted publishing (no stored token) with provenance attestations: bump the version in pyproject.toml and moodle_mcp/__init__.py, add a CHANGELOG entry, merge, push the tag and approve the pypi deployment.
Security
Report vulnerabilities privately as described in SECURITY.md — never in public issues or group chats.
License
MIT — see LICENSE. The optional [pdf] extra installs PyMuPDF, which is AGPL-3.0 and carries its own obligations.
Metadata
Release files for moodle-obsidian-mcp 0.8.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| moodle_obsidian_mcp-0.8.1.tar.gz | 119.1 kB | With provenance | Details for moodle_obsidian_mcp-0.8.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| moodle_obsidian_mcp-0.8.1-py3-none-any.whl | Python 3 | none | any | Details for moodle_obsidian_mcp-0.8.1-py3-none-any.whl |
Total release size: 212.4 kB
Release files / moodle_obsidian_mcp-0.8.1.tar.gz
| Download URL | moodle_obsidian_mcp-0.8.1.tar.gz |
|---|---|
| Size | 119.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1f3996716d828b6d8ba9639cd3892946daa72e86a6972897ad501beb3a77e42f
|
|
BLAKE2b-256 checksum How to use checksums |
670d2fb771061ad5ce20f82befaaf9c74c052e0a98ccaf235be0cb7f9e49632e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / moodle_obsidian_mcp-0.8.1-py3-none-any.whl
| Download URL | moodle_obsidian_mcp-0.8.1-py3-none-any.whl |
|---|---|
| Size | 93.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8f54cc11ae51477f02a0389e2152ef55712b6341364884fc1cebd14af50128ae
|
|
BLAKE2b-256 checksum How to use checksums |
a8d2f4674e540d916d933f49dd20a622c81bde4628cf739aa212a874507c2775
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for moodle-obsidian-mcp 0.8.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for moodle_obsidian_mcp-0.8.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.