Totara MCP server
A FastMCP server that lets Cursor (and other MCP clients) call Totara 20 external GraphQL only. It exposes three tools: totara_list_courses, totara_create_user, and totara_enrol_user. Legacy Totara REST (/webservice/rest/server.php) and course creation are out of scope for this MVP.
The server speaks stdio only. Cursor starts a local process and talks to it over stdin/stdout. There is no remote HTTP MCP endpoint.
Requirements
- A Totara 17+ site with an OAuth 2.0 client credentials client under Quick-access menu → Development → API
- The OAuth client must be linked to a Totara user who can list courses, create users, and assign the enrolment role you will use
Plus, depending on how you run the server:
| Audience | Needs |
|---|---|
| End users | uv (uvx can fetch Python 3.15) |
| Developers (this repo) | Docker and Docker Compose |
End users (published package)
Install uv, then copy examples/cursor-mcp.json into ~/.cursor/mcp.json (or merge the totara entry into an existing file):
{
"mcpServers": {
"totara": {
"type": "stdio",
"command": "uvx",
"args": ["totara-mcp", "--transport", "stdio"],
"env": {
"TOTARA_BASE_URL": "https://your-totara.example.com",
"TOTARA_CLIENT_ID": "your-client-id",
"TOTARA_CLIENT_SECRET": "your-client-secret"
}
}
}
}
- Replace the three
envvalues with your Totara site URL and OAuth client credentials - Reload MCP (Customize → MCPs) and confirm the
totaraserver shows green
uvx totara-mcp installs the package from PyPI and can download Python 3.15 to satisfy requires-python >= 3.15. You do not need Docker or this git clone.
Developers (this repo)
Python, uv, and package dependencies stay inside the image. The host only needs Docker.
Configure
cp .env.example .env
Edit .env (never commit it):
| Variable | Purpose |
|---|---|
TOTARA_BASE_URL |
Totara site root URL, no trailing slash e.g. http://demo-totara-txp.totara84.localhost |
TOTARA_CLIENT_ID |
OAuth client id from Development → API |
TOTARA_CLIENT_SECRET |
OAuth client secret |
Auth flow used by the server:
POST {TOTARA_BASE_URL}/totara/oauth2/token.phpwithgrant_type=client_credentialsPOST {TOTARA_BASE_URL}/api/graphql.phpwithAuthorization: Bearer …
Build once
docker compose build
Rebuild after Dockerfile or uv.lock changes. Do not put --build in Cursor MCP config; Cursor restarts the process often.
Cursor
This repo ships .cursor/mcp.json. Cursor starts the container with Compose and speaks stdio. Totara credentials come from .env via Compose; the committed file has no secrets.
{
"mcpServers": {
"totara": {
"type": "stdio",
"command": "docker",
"args": [
"compose",
"-f",
"${workspaceFolder}/docker-compose.yml",
"run",
"-i",
"-T",
"--rm",
"totara-mcp",
"totara-mcp",
"--transport",
"stdio"
]
}
}
}
- Fill in
.envand rundocker compose build - Open this project as the Cursor workspace
- Reload MCP (Customize → MCPs) and confirm the
totaraserver shows green
While this workspace is open, the project .cursor/mcp.json wins over any totara entry in ~/.cursor/mcp.json, so you exercise this checkout, not the last published PyPI version.
Tools
Each tool sends one custom GraphQL document to /api/graphql.php and returns a short JSON object. GraphQL errors (often still HTTP 200) become a tool error. HTTP 401 refreshes the OAuth token once and retries that call once.
totara_list_courses
Calls Totara query core_course_courses.
| Argument | Required | Description |
|---|---|---|
limit |
no | Page size (default 20) |
cursor |
no | Opaque next_cursor from a previous page |
since_timemodified |
no | Filter: unix timestamp, numeric string, or ISO-8601 date |
Sample call:
{
"limit": 20,
"cursor": null,
"since_timemodified": null
}
Sample result:
{
"items": [
{
"id": "2",
"fullname": "Introduction",
"shortname": "intro",
"idnumber": "C1"
}
],
"total": 1,
"next_cursor": ""
}
totara_create_user
Calls Totara mutation core_user_create_user. Other profile fields (address, custom fields, tenant, and so on) are intentionally not exposed.
The MCP server always sends generate_password: true. Totara creates a random password and emails it to the user. Callers never supply a password.
username, email, firstname, and lastname are required to create the user, but they are optional on the tool call. Pass a field only when the user explicitly supplied it. Do not invent a username, email, or name. If any of those four is omitted, the tool asks the user (MCP elicitation) before calling Totara. If the client cannot show that form, the tool returns an error listing the missing fields and does not create a user. Declining or cancelling the form also creates no user.
| Argument | Required | Description |
|---|---|---|
username |
yes, asked if omitted | Unique username. Do not invent one |
email |
yes, asked if omitted | Unique email. Do not invent one |
firstname |
yes, asked if omitted | First name. Do not invent one |
lastname |
yes, asked if omitted | Last name. Do not invent one |
idnumber |
no | External id number. Omit unless the user supplied it |
auth |
no | Auth plugin shortname (for example manual). Omit unless the user supplied it |
Sample call:
{
"username": "learner1",
"email": "learner1@example.com",
"firstname": "Lea",
"lastname": "Ner",
"auth": "manual"
}
Sample result:
{
"id": "10",
"username": "learner1",
"email": "learner1@example.com",
"firstname": "Lea",
"lastname": "Ner",
"idnumber": null
}
totara_enrol_user
Calls Totara mutation enrol_manual_enrol_user.
Provide exactly one user identifier and exactly one course identifier. Omit role_shortname to use the enrol instance default (usually Learner).
| Argument | Required | Description |
|---|---|---|
user_id |
one of user_* | User database id (string or integer) |
user_username |
one of user_* | Username |
user_idnumber |
one of user_* | User idnumber |
user_email |
one of user_* | User email |
course_id |
one of course_* | Course database id (string or integer) |
course_shortname |
one of course_* | Course shortname |
course_idnumber |
one of course_* | Course idnumber |
role_shortname |
no | Role shortname (for example student) |
Sample call:
{
"user_username": "learner1",
"course_shortname": "intro",
"role_shortname": "student"
}
Sample result:
{
"success": true,
"was_already_enrolled": false,
"user": {
"id": "10",
"username": "learner1",
"idnumber": null,
"email": "learner1@example.com"
},
"course": {
"id": "2",
"shortname": "intro",
"idnumber": "C1"
},
"role": {
"id": "5",
"shortname": "student"
}
}
Try it (Inspector)
Over stdio (developers, after docker compose build):
npx @modelcontextprotocol/inspector \
docker compose -f docker-compose.yml run -i -T --rm totara-mcp totara-mcp --transport stdio
Exercise tools in this order:
totara_list_courses— confirm courses come backtotara_create_user— create a disposable test usertotara_enrol_user— enrol that user into a listed course- Call
totara_enrol_useragain with no user identifier — confirm the validation error path
Do not point Inspector at a production Totara site while experimenting with create/enrol.
(npx is only for the optional Inspector UI. The MCP server itself does not require Node on the host.)
Tests
Run the test suite inside the container:
docker compose --profile test run --rm --build test
Tests mock Totara over HTTP. They do not call a live site. Coverage includes: OAuth token request and cache, GraphQL document/variables for each tool, GraphQL errors → tool error, and one refresh-and-retry on HTTP 401.
Publish (PyPI)
Package name on PyPI: totara-mcp. The GitHub repo can stay private; PyPI only receives the built sdist and wheel.
- Create a PyPI account, enable 2FA, and create an API token
- Optional dry run against TestPyPI:
uv build
uv publish --publish-url https://test.pypi.org/legacy/
- Production:
uv build
uv publish
- Verify on a machine without this checkout:
uvx totara-mcp --help
Later releases: bump version in pyproject.toml (PyPI never overwrites an existing version), then build and publish again. Totara credentials are never part of the package; they stay in each user’s mcp.json or .env.
Layout
totara-mcp/
pyproject.toml
uv.lock
LICENSE
Dockerfile # runtime + test image targets
docker-compose.yml # totara-mcp (stdio) + test profile
.env.example
.cursor/mcp.json # developers: Compose stdio
examples/
cursor-mcp.json # end users: copy into ~/.cursor/mcp.json
README.md
src/totara_mcp/
server.py # FastMCP app; stdio
config.py # Environment settings
oauth.py # Client-credentials token cache
graphql.py # POST /api/graphql.php + 401 retry
tools.py # The three MCP tools
tests/
test_oauth.py
test_tools.py
Metadata
Release files for totara-mcp 0.1.0
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 | |
|---|---|---|---|---|
| totara_mcp-0.1.0.tar.gz | 10.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| totara_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 23.1 kB
Release files / totara_mcp-0.1.0.tar.gz
| Download URL | totara_mcp-0.1.0.tar.gz |
|---|---|
| Size | 10.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6ee451df87575258954044fb29bf02d6283d88785fe423977361f1dcc0bcf3d7
|
|
BLAKE2b-256 checksum How to use checksums |
a3fa606f6b4e7d5bfd56ea4cc0a246b9f1dfe542f3490a83434d5d59eb0af0cc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / totara_mcp-0.1.0-py3-none-any.whl
| Download URL | totara_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 12.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3571d3a97300f29abb6914cf6b09b6e7c8338feba7c47fc94e061d979c650014
|
|
BLAKE2b-256 checksum How to use checksums |
3ca27b978dda8a75e5b09969dd17512a187c8902d7a55b49db488827ce54f1d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for totara-mcp 0.1.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A 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.