Skip to main content

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"
      }
    }
  }
}
  1. Replace the three env values with your Totara site URL and OAuth client credentials
  2. Reload MCP (Customize → MCPs) and confirm the totara server 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:

  1. POST {TOTARA_BASE_URL}/totara/oauth2/token.php with grant_type=client_credentials
  2. POST {TOTARA_BASE_URL}/api/graphql.php with Authorization: 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"
      ]
    }
  }
}
  1. Fill in .env and run docker compose build
  2. Open this project as the Cursor workspace
  3. Reload MCP (Customize → MCPs) and confirm the totara server 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:

  1. totara_list_courses — confirm courses come back
  2. totara_create_user — create a disposable test user
  3. totara_enrol_user — enrol that user into a listed course
  4. Call totara_enrol_user again 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.

  1. Create a PyPI account, enable 2FA, and create an API token
  2. Optional dry run against TestPyPI:
uv build
uv publish --publish-url https://test.pypi.org/legacy/
  1. Production:
uv build
uv publish
  1. 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)

Source distribution for totara-mcp 0.1.0
File Size Uploaded Provenance
totara_mcp-0.1.0.tar.gz 10.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for totara-mcp 0.1.0
File Interpreter ABI Platform
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page