moodle-watch
Provides a stateful, read-only memory layer over a Moodle instance via a moodle-dl mirror: tools for seeing what is new/changed/vanished since a timestamp or date (whats_new), listing watched and pending courses with the policy rule that decided them, tracking deadlines and due-date changes, working through a backlog of unhandled items, inspecting a course tree annotated with item state, accent-insensitive full-text search over course/section/module/file names, reading document text, viewing the revision history of items and who handled them, marking items handled per revision, setting course policies, and running background sync cycles (detect-only or with downloads).
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@moodle-watch what's new in my Moodle since Tuesday?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
moodle-watch
Give your AI agents a memory of your Moodle.
Moodle can tell an agent what exists. It cannot tell it what is new to you, what changed since you last looked, or what you have already dealt with. Every Moodle MCP server out there is a stateless wrapper around the same API: ask it twice, get the same answer twice, learn nothing.
moodle-watch keeps a ledger. That one difference changes what an agent can do.
Why this matters if you work with agents
A stateless tool makes your agent a search box. You ask, it fetches, it forgets. You are still the one who remembers what you already read, notices that a lecturer silently re-uploaded a PDF, and keeps the mental list of what is left. That bookkeeping is the actual work, and no amount of prompting removes it.
With a ledger, the agent holds all three:
It can open with news instead of waiting for a question. whats_new is a
delta, not a dump. "Since Tuesday: four new files in two courses, one deadline
moved a week earlier, and a course you have never triaged just appeared."
It stops re-reading what it already read. Every item carries its handling
state. A nightly agent can ask for the backlog, work through it, call
mark_handled with a link to whatever it produced, and pick up exactly where it
left off tomorrow. No "have I seen this already" heuristics, no re-summarising
the same lecture four times, no burned context.
It notices the thing you would have missed. A lecturer replaces a slide deck two days before the exam. moodle-dl overwrites the file. Moodle sends nothing. Here, the item gets a new revision, and because handling is recorded per revision, it returns to your backlog by itself. You never diff anything by hand again.
A swarm can divide the work. mark_handled takes a by field. Several
agents, or an agent and a human, can share one queue without stepping on each
other, and history shows who did what to which revision.
It never decides behind your back. A newly discovered course lands in
pending: not watched, not ignored, surfaced on every call until somebody
arbitrates. Every course carries the rule that decided it, so "why is this not
showing up" always has an answer. Silent whitelists are how you find out in
March that a module was never being tracked.
And it is read-only against Moodle by construction, not by instruction. The web-service allowlist lives in code; submitting, grading and posting are unreachable. You can hand this to an autonomous agent without wondering what it might click.
Related MCP server: Claude Synergy
How it works
It does not reimplement moodle-dl. It imports it.
moodle-dl what exists, what changed mods, LTI extractors, yt-dlp
moodle-watch since when, what you did the clock, the ledger, MCPEvery course module type, every LTI video extractor, yt-dlp, years of awkward real-world cases: all of it comes along. moodle-watch adds the two things moodle-dl does not have.
Because moodle-dl records what it has, not when it got it. Its
time_stamp column is written only on modification paths; a fresh insert leaves
the default. On a real mirror of 2210 rows, and again on a fresh one of 95,
every single value is 0. And once its notified flag is raised, the seen /
not-seen distinction is gone for good.
So the ledger records three things:
| our own clock, on every revision |
| who did what with an item, per revision |
| why a course is watched or not, and which rule decided |
Handling is keyed on moodle-dl's file_id, and moodle-dl inserts a new row
whenever a file changes. The "it comes back to your backlog" behaviour is not a
feature anyone maintains; it is a property of the schema.
Nothing is ever deleted. A disappearance is recorded as an observation.
Install
pipx install "moodle-watch[documents]"You need a moodle-dl mirror directory. If you do not have one:
moodle-dl --init --sso --path ~/Moodle # or --init -u USER -pw PASSThen point moodle-watch at it and run a first cycle:
export MOODLE_WATCH_MIRROR=~/Moodle
moodle-watch run
moodle-watch coursesThe first run discovers every course you are enrolled in and puts them all in
pending. Decide with a policy file, or one tool call per course.
Tools
Tool | Answers |
| what appeared, changed or vanished since |
| what is watched, what is pending, and the rule that decided |
| what is due, and whether a due date moved |
| everything not handled yet, oldest first |
| sections and modules, each annotated with its state |
| accent-insensitive full text over course, section, module, file names |
| the text of one file, from the mirror or through the token |
| every revision of an item, and everything done with it |
| record that items were dealt with |
| answer a pending course |
| start a cycle in the background, poll it |
Watching and mirroring are the same code with a different argument:
moodle-watch run detects only, moodle-watch run --fetch also downloads,
forums and videos included.
The policy
~/.config/moodle-watch/policy.toml, written for you on first run. Rules are
evaluated in order, first match wins, like a firewall.
default = "ask" # ask | watch | ignore
[[rule]]
match = { startdate_after = "2026-08-01" } # this year only
action = "watch"
[[rule]]
match = { shortname = "*2025*" }
action = "ignore"
[exclude]
extensions = ["mp4"]
larger_than = "500MB"
filenames = ["department-logo.png"] # moodle-dl cannot filter by nameCriteria: course, shortname, fullname, startdate_after,
startdate_before. The [exclude] block is written into moodle-dl's own
config.json, so there is one source of truth. A decision you make by hand is
never overwritten by a rule afterwards.
Running it
Over stdio, for a local client:
{ "mcpServers": { "moodle-watch": {
"command": "moodle-watch-server",
"env": { "MOODLE_WATCH_MIRROR": "/home/you/Moodle" } } } }Over HTTP, behind your own reverse proxy:
MOODLE_WATCH_HOSTS=moodle.example.org moodle-watch-server --httpIt binds loopback only. MOODLE_WATCH_HOSTS feeds the SDK's DNS-rebinding
guard, which validates the Host header; the public name must be listed there
or the proxy gets a 421. See docs/deployment.md and the
templates in deploy/.
Environment
Variable | Meaning |
| the moodle-dl directory. Required. |
| ledger path. Default: |
| policy file. Default: |
| HTTP port. Default: |
| comma-separated public host names |
| only for |
No secret lives in this repository. The Moodle token stays in the mirror's
config.json, where moodle-dl put it.
Diagnosing
moodle-watch doctor # layers 1 to 3
moodle-watch doctor --remote # and the public doorFour layers, deepest to most exposed. Layer 3 is not a health check: it opens a
real MCP session and asserts the answers are coherent with each other. A server
returning 200 while reporting empty courses is not a server that works. The
output never prints the secret URL segment.
When the token expires
Moodle web-service tokens expire, and an SSO-issued one is renewed by hand:
moodle-dl -nt -sso --path ~/Moodlemoodle-watch names this failure mode rather than dying quietly: the tools return
a token_expired payload carrying that exact command, doctor gives it its own
line, and a failed run records it in the ledger.
Honest status
This works. It is also vibe-coded. It was built in one sitting, with an AI,
by someone who is a teacher and not a software maintainer. It is verified rather
than merely hoped for: every claim above was checked against a live Moodle 4.5
instance, there are 40 tests, and the load-bearing behaviours (idempotence,
revisions returning to the backlog, nothing ever deleted, the Host guard) each
have one. Two real design bugs were caught by those tests and fixed. But there
is no roadmap, no support, and no promise of a second sitting.
One thing in particular deserves a warning: moodle-watch drives moodle-dl
through internal classes of a command-line application, which carry no
stability guarantee. That is why the version is pinned, why every import is
confined to engine.py, and why doctor asserts each call site still exists.
It is a deliberate and contained bet, not an oversight, but it is a bet.
So: shared gladly, take it, fork it, do whatever you like with it. If you think the idea is good, the right move is to maintain your own copy, not to wait on this one. Issues may go unanswered. A fork that outgrows this repo would be a happy outcome, not a slight.
Licence
GPL-3.0-or-later, inherited from moodle-dl, which is imported rather than reimplemented.
Available Tools
12 toolsbacklogB
Everything not handled yet, oldest first. The work queue.
An item leaves this list when mark_handled records a read, ingested or
ignored action for it. Marking it todo keeps it here on purpose.
Inputs: limit, course. Returns: result
coursesA
List watched courses, with unhandled counts and the next deadline.
Each course carries the rule that decided its state, so "why does this
course not show up" always has an answer. state is one of watch, ignore,
or pending; a pending course has never been arbitrated by anyone.
Inputs: include_ignored. Returns: result
course_treeA
Sections and modules of one course, each item annotated with its state.
Use this to navigate a course rather than to search it. For a whole-space
lookup by name, use search.
Inputs: course (required), only_unhandled. Returns: result
deadlinesC
Upcoming deadlines, soonest first.
moved is true when the due date changed since it was first seen, and
moved_from gives the previous one. No Moodle API reports this; it is known
only because the ledger saw the earlier value.
Inputs: within, include_past. Returns: result
historyA
Every revision of one item, and everything that was done with it.
item_key comes from any other tool's output. Revisions follow the file
across changes and moves, so this is the honest answer to "has this been
updated since I read it".
Inputs: item_key (required). Returns: result
mark_handledA
Record that these items were dealt with. Writes the ledger, not Moodle.
action is one of read, ingested, ignored, todo. by is free text naming
who did it, for example nightly or alice. note is the right place
for a link to whatever was produced.
Handling is per revision: if the file changes later, it returns to the backlog on its own.
Inputs: by (required), action (required), file_ids (required), note. Returns: result
read_documentA
Extract the text of one file, by its file_id.
Reads the local copy when the mirror has it, and downloads through the
Moodle token otherwise. pages selects PDF pages, for example 1-5 or 3.
Media files return their metadata and no text: a video is not summarised
here.
Inputs: file_id (required), pages, max_chars. Returns: result
searchA
Full-text search over course, section, module and file names.
Accent-insensitive: evaluation finds Évaluation. Supports FTS5 syntax,
so dimension NEAR langagiere and program* work.
Inputs: query (required), limit, course. Returns: result
set_course_policyA
Decide whether a course is watched. Use this to answer a pending course.
state is watch, ignore or pending. A decision made with a by other than
policy is never overwritten by a policy rule afterwards, so the user's
answer sticks.
Inputs: by (required), state (required), course_id (required), reason. Returns: result
syncA
Start a watch cycle in the background and return at once.
A full cycle takes minutes, so it never runs inside a tool call. Poll
sync_status for the result. With download true it also fetches files,
which takes considerably longer.
Inputs: download. Returns: result
sync_statusA
The state of the most recent sync, running or finished.
finished null means it is still going. A non-null error starting with
token_expired means the Moodle token must be renewed by hand; report the
command rather than retrying.
Inputs: no named parameters. Returns: result
whats_newA
What appeared, changed or vanished since since. The main tool.
since accepts last_run (the default), all, a duration like 7d or
48h, an ISO date like 2026-09-01, or an epoch integer.
Already handled items are hidden unless include_handled is true: the
question asked is what remains, not what happened. The reply also carries
deadline changes (a moved_from means the due date was shifted) and
pending_courses, which need the user's arbitration.
Inputs: limit, since, course, include_handled. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 12 tools
The tools are mostly distinct in purpose—listing courses, diffing changes, showing deadlines, searching, reading, handling, and syncing. A few overlaps exist, such as whats_new also surfacing deadline changes and pending courses, and backlog overlapping with whats_new's unhandled view, but the descriptions are explicit enough to resolve ambiguity.
All names are lowercase and use underscores, but the convention is mixed: plural nouns (courses, deadlines), bare verbs (search, sync), and verb+noun compounds (read_document, mark_handled, set_course_policy) coexist. The names are readable but not predictable from a single pattern.
Twelve tools is well-scoped for a Moodle change-watching service, covering distinct phases from course policy and syncing to change discovery, reading, history, and handling. Each tool earns its place without obvious bloat or trivial additions.
The core workflow is covered end-to-end: sync, see what changed, inspect course structure and content, read files, mark items handled, and set course policy. Minor workarounds exist, such as no explicit undo of a handling action, but they do not block the main use case.
Maintenance
Related MCP Connectors
Versioned agent memory in your own Postgres: portable context, permissioned, audit trail.
Persistent, inspectable memory for AI agents with lineage, correction, and a hosted MCP endpoint.
Provenance-carrying memory for AI agents: corrections supersede, never overwrite. Free, API + MCP.
Long-term memory for AI agents: bitemporal fact ledger, contradiction detection, explainability.
Related MCP Servers
- AlicenseAqualityBmaintenanceChange tracking for AI-era codebases. AI agents call it to log structured change events (entity + diff + reasoning) before the session ends, then query history with diff, blame, history, changeset, and search. Captures the intent that would otherwise evaporate.8188 PyPI26MIT
- AlicenseNot gradedqualityBmaintenanceProvides a local, queryable mirror of changelogs from Anthropic and related AI dev tools, enabling agents to discover cross-product workflows and sync updates via MCP.32 npmMIT
- AlicenseNot gradedqualityBmaintenanceExposes a self-hosted shared workspace over MCP where AI agents and their teammates read and write tasks, decisions, docs and results, so each session picks up where the last one left off. It adds hybrid cited search, per-workspace OAuth or scoped tokens, change feeds for cheap re-sync, and an append-only audit trail of every agent or human write.AGPL 3.0
- AlicenseNot gradedqualityCmaintenanceEnables any MCP client (zcode, Claude Code, Cursor) to give its model bi-temporal long-term memory, auditable evidence-cited retrieval, LLM-assisted document extraction, and deterministic A* skill planning with precondition/effect verification. All four capabilities share one portable SQLite store and can be mounted independently on demand.3MIT