Skip to main content
Glama

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, MCP

Every 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:

first_seen

our own clock, on every revision

handled

who did what with an item, per revision

course_policy

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 PASS

Then point moodle-watch at it and run a first cycle:

export MOODLE_WATCH_MIRROR=~/Moodle
moodle-watch run
moodle-watch courses

The 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

whats_new

what appeared, changed or vanished since last_run, 7d, a date

courses

what is watched, what is pending, and the rule that decided

deadlines

what is due, and whether a due date moved

backlog

everything not handled yet, oldest first

course_tree

sections and modules, each annotated with its state

search

accent-insensitive full text over course, section, module, file names

read_document

the text of one file, from the mirror or through the token

history

every revision of an item, and everything done with it

mark_handled

record that items were dealt with

set_course_policy

answer a pending course

sync / sync_status

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 name

Criteria: 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 --http

It 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

MOODLE_WATCH_MIRROR

the moodle-dl directory. Required.

MOODLE_WATCH_DB

ledger path. Default: <mirror>/moodle_watch.db

MOODLE_WATCH_POLICY

policy file. Default: ~/.config/moodle-watch/policy.toml

MOODLE_WATCH_PORT

HTTP port. Default: 3041

MOODLE_WATCH_HOSTS

comma-separated public host names

MOODLE_WATCH_BASE_URL, MOODLE_WATCH_SEGMENT

only for doctor --remote

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 door

Four 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 ~/Moodle

moodle-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 tools
backlogB

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

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

    A3.6/5.0

    Scored across 12 tools

    Disambiguation4/5

    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.

    Naming Consistency3/5

    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.

    Tool Count5/5

    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.

    Completeness4/5

    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

    ActivityStale
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      Change 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.
      8
      188 PyPI
      26
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Exposes 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
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables 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.
      3
      MIT