{"id":"d64f5b54-8d37-4327-b5b2-1197bf99cd8c","slug":"learnworlds-mcp","kind":"mcp","name":"Learnworlds-MCP","summary":"ohneben's Model Context Protocol server for the LearnWorlds API — all 94 endpoints as safety-categorized MCP tools (read-only / write / destructive). Runs over stdio or Streamable HTTP, with Docker, r","owner":"ohneben","repoUrl":"https://github.com/ohneben/Learnworlds-MCP","license":"MIT","licenseTier":"permissive","trust":"community","stars":2,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-07-07T08:20:24.000Z","addedAt":"2026-10-14T05:51:13.017Z","logoUrl":"https://avatars.githubusercontent.com/u/151868236?u=bd673b1c4a89515db3489d43e1b9eb465da29177&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","memory-knowledge"],"tags":["api","claude","cursor","docker","e-learning","learnworlds","lms","online-courses"],"runtime":"docker","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":true,"reviewed":false,"syncedAt":"2026-10-17T17:38:49.441Z","sample":false,"url":"https://conduits.dev/tools/learnworlds-mcp","language":"TypeScript","transports":["stdio"],"install":{"docker":"ghcr.io/ohneben/learnworlds-mcp:1.1.2"},"requirements":{"env":[{"name":"MCP_TRANSPORT","secret":false,"required":true,"description":"Server transport. Set to 'stdio' for local MCP clients; the image otherwise starts the Streamable HTTP transport."},{"name":"LEARNWORLDS_BASE_URL","secret":false,"required":true,"description":"Base URL of your LearnWorlds school's API, e.g. https://<your-school>.learnworlds.com/admin/api"},{"name":"LEARNWORLDS_API_TOKEN","secret":true,"required":true,"description":"Bearer access token (LearnWorlds admin -> Settings -> Integrations -> Developers)."},{"name":"LEARNWORLDS_CLIENT_ID","secret":true,"required":true,"description":"Your school's Client ID, sent as the Lw-Client header."},{"name":"MCP_AUTH_TOKEN","secret":true,"required":false,"description":"Only for MCP_TRANSPORT=http: bearer token required on /mcp. Without it a non-loopback bind refuses to start."}],"upstreamMayCharge":false},"tools":null,"readme":"# ohneben's LearnWorlds MCP\n\n[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-ohneben-FFDD00?style=for-the-badge&logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/ohneben)\n\n---\n\n#### License & checks\n\n[![CI](https://github.com/ohneben/Learnworlds-MCP/actions/workflows/ci.yml/badge.svg)](https://github.com/ohneben/Learnworlds-MCP/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE.md)\n\n#### MCP registries\n\n[![MCP Registry](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fregistry.modelcontextprotocol.io%2Fv0.1%2Fservers%2Fio.github.ohneben%252Flearnworlds-mcp%2Fversions%2Flatest&query=%24.server.version&prefix=v&label=MCP%20Registry&color=blue&logo=modelcontextprotocol&logoColor=white)](https://registry.modelcontextprotocol.io/v0.1/servers/io.github.ohneben%2Flearnworlds-mcp/versions/latest)\n[![Listed on mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/ohneben/learnworlds-mcp)\n[![Learnworlds-MCP MCP server](https://glama.ai/mcp/servers/ohneben/Learnworlds-MCP/badges/score.svg)](https://glama.ai/mcp/servers/ohneben/Learnworlds-MCP)\n\nRun your [LearnWorlds](https://www.learnworlds.com/) school in plain language from AI\nassistants like **Claude**, **Cursor**, and any other\n[MCP](https://modelcontextprotocol.io) client.\n\nThis [Model Context Protocol](https://modelcontextprotocol.io) server exposes the\n**LearnWorlds public API** — all **94 endpoints**, generated straight from the\nOpenAPI spec into MCP tools. Every tool is **safety-categorized**\n(🟢 read-only / 🟡 write / 🔴 destructive) so your assistant knows what an action does\n*before* it calls it. It runs over **stdio** (Claude Desktop and other local launchers)\nor **Streamable HTTP** (hosted in Docker), and ships with retries, client-side rate\nlimiting, and request timeouts so it holds up against a live school.\n\n## Why you'll want this\n\nSome MCP servers just forward an API. This one is built to be **safe to hand to an\nLLM** and **easy to run for real**:\n\n| What you get | Why it matters |\n| --- | --- |\n| **All 94 endpoints, spec-driven** | Full coverage of courses, users, enrollments, payments, subscriptions, coupons, certificates, seats, community and reporting — nothing hand-picked or left behind. |\n| **Every tool is safety-categorized** 🟢 / 🟡 / 🔴 | A banner at the top of each tool description tells the model exactly what it does — read, create, update or delete — before it acts. |\n| **Descriptions written for agents, not humans** | Every tool spells out its side effects, auth and rate-limit behavior, return shape, error codes, when *not* to reach for it, and which sibling tools are the alternatives. |\n| **Machine-readable MCP annotations** (`readOnlyHint`, `destructiveHint`) | Hosts that honor annotations (Claude included) can auto-trust reads and demand confirmation before anything destructive. |\n| **Automatic retries with backoff** | Transient `429` / `5xx` responses are retried with jittered exponential backoff, honoring the server's `Retry-After` header. |\n| **Built-in rate limiting** | Self-throttles under LearnWorlds' **30 requests / 10 s** cap so a burst of tool calls never trips a `429`. |\n| **Per-request timeouts** | A hung upstream call is aborted and retried instead of freezing the server. |\n| **Two transports: stdio *and* Streamable HTTP** | Use it locally in Claude Desktop, or run one always-on server that any number of MCP clients reach over HTTP. |\n| **Docker + docker-compose, health check, auto-restart** | Production-style deployment out of the box: `docker compose up` and it stays up. |\n| **Bearer-token auth** on the HTTP endpoint | Required as soon as the server is bound beyond loopback: without `MCP_AUTH_TOKEN` it refuses to start rather than exposing the API unprotected. |\n| **Your secrets never reach the model** | Credentials live in the server's environment and are injected on every request — the assistant only ever sees tool inputs and API responses. |\n| **Drop-in spec updates** | LearnWorlds ships a newer YAML? Replace one file and rebuild — new endpoints become new tools automatically, no code changes. |\n\n### How it compares\n\nAt the time of writing this appears to be the only dedicated LearnWorlds MCP server.\nYou *could* instead point a generic OpenAPI→MCP wrapper at the spec — here's what\nthat leaves on the table:\n\n| Capability | **This project** | Generic OpenAPI→MCP wrapper\\* |\n| --- | :---: | :---: |\n| All 94 LearnWorlds endpoints as tools | ✅ | ✅ |\n| Per-tool 🟢 / 🟡 / 🔴 safety category + banner | ✅ | ❌ |\n| `readOnlyHint` / `destructiveHint` MCP annotations | ✅ | ➖ |\n| `$ref` dereferencing + recursion-safe schemas | ✅ | ➖ |\n| Automatic retries on `429` / `5xx` (honors `Retry-After`) | ✅ | ❌ |\n| Client-side rate limiting (stays under 30 req / 10 s) | ✅ | ❌ |\n| Per-request timeout with abort | ✅ | ➖ |\n| `stdio` transport | ✅ | ✅ |\n| **Streamable-HTTP transport** | ✅ | ➖ |\n| **Docker + docker-compose**, health check, auto-restart | ✅ | ❌ |\n| **Enforced bearer-token auth** on the endpoint | ✅ | ❌ |\n| Credentials injected server-side, never sent to the model | ✅ | ➖ |\n| License | MIT | varies |\n\n<sub>\\*Generic OpenAPI→MCP wrappers turn any Swagger/OpenAPI spec into MCP tools. They\ncan reach the same endpoints, but treat every operation identically — no safety\ncategories, no resilience, no deployment story, and no guardrails tuned for live\nschool data. \"➖\" = varies by tool / not guaranteed. Snapshot from July 2026.</sub>\n\n## What you can do\n\nOnce it's connected, ask your assistant things like:\n\n- \"List the 10 most recent users who signed up this month.\"\n- \"Create a user for jane@example.com and enroll her in the 'Pro' bundle.\"\n- \"Show me this month's payments and total revenue.\"\n- \"Which users haven't completed the 'Onboarding' course yet?\"\n- \"Create a 20%-off coupon for the annual subscription plan.\"\n- \"Pull completion analytics for our top 5 courses.\"\n\nTools are generated automatically from the API and grouped into 🟢 read-only,\n🟡 write, and 🔴 destructive — so a well-behaved host can treat each group differently.\n\n## How it works\n\n```\nClaude / Cursor / any MCP client  ──MCP──►  this server  ──HTTPS──►  LearnWorlds API (your school)\n```\n\nThe server parses the bundled OpenAPI spec into MCP tools (resolving `$ref`s and\nguarding against recursive schemas), tags each with its safety category, and injects\nyour bearer token and `Lw-Client` header on every outgoing request. Your credentials\nstay in the server's environment — the model never sees or handles them.\n\n## Requirements\n\n- A **LearnWorlds school with API access** — an **access token** and a **Client ID**\n  (admin → Settings → Integrations → Developers), plus your school's API base URL.\n  See [Get your API credentials](#get-your-api-credentials).\n- **Docker** (Docker Desktop on macOS/Windows) for the quick start below — or\n  **Node.js ≥ 18** to [run from source](#run-from-source-stdio-no-docker).\n\n## Quick start (Docker)\n\n**1. Add your credentials.** Copy the example config and fill it in:\n\n```bash\ncp .env.example .env\n# edit .env → set LEARNWORLDS_BASE_URL, LEARNWORLDS_API_TOKEN, LEARNWORLDS_CLIENT_ID\n#           → set MCP_AUTH_TOKEN (required once the port is published beyond\n#             127.0.0.1): openssl rand -hex 32\n```\n\n**2. Start the server:**\n\n```bash\ndocker compose up -d --build\n```\n\nThe bundled `docker-compose.yml` binds to `127.0.0.1:8765` only, so the server is\nreachable from your machine but not the network.\n\n**3. Confirm it's running:**\n\n```bash\ncurl -s http://localhost:8765/health\n# → {\"status\":\"ok\",\"server\":\"learnworlds-mcp\",\"tools\":94}\n```\n\n**4. Connect your MCP client.** The MCP endpoint is `http://localhost:8765/mcp`.\n\n- **Claude Desktop** — add a **custom connector** (Settings → Connectors) pointing at\n  the URL, or bridge it locally with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote).\n  Add this under `mcpServers` in your config, then fully quit and reopen the app:\n\n  ```json\n  {\n    \"mcpServers\": {\n      \"learnworlds\": {\n        \"command\": \"npx\",\n        \"args\": [\n          \"mcp-remote\",\n          \"http://localhost:8765/mcp\",\n          \"--header\", \"Authorization: Bearer YOUR_MCP_AUTH_TOKEN\"\n        ]\n      }\n    }\n  }\n  ```\n\n  (Drop the `--header` line if you left `MCP_AUTH_TOKEN` empty.)\n\n- **Claude Code** — one command:\n\n  ```bash\n  claude mcp add --transport http learnworlds http://localhost:8765/mcp\n  ```\n\n- **Claude Cowork** — shares Claude Code's MCP config, so the command above makes the\n  tools available there too.\n\n### Prefer a prebuilt image?\n\nEvery push to `main` publishes a ready-to-run image to the GitHub Container Registry,\nso you can skip the local build entirely:\n\n```bash\ndocker run -d --name learnworlds-mcp -p 127.0.0.1:8765:8765 --env-file .env \\\n  ghcr.io/ohneben/learnworlds-mcp:latest\n```\n\n## Get your API credentials\n\n1. Log in to your LearnWorlds school admin.\n2. Go to **Settings → Integrations → Developers** (the API screen).\n3. Copy your **Access Token** → `LEARNWORLDS_API_TOKEN`, and your **Client ID** →\n   `LEARNWORLDS_CLIENT_ID`.\n4. Set `LEARNWORLDS_BASE_URL` to your school's API base:\n   `https://<your-school>.learnworlds.com/admin/api`. If your school uses a custom\n   domain, use that host instead (e.g. `https://academy.example.com/admin/api`).\n\nPut all three in `.env`. The server injects them on every request, so your assistant\nnever sees them.\n\n## Configuration\n\nEverything is set in `.env` (copied from `.env.example`):\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `LEARNWORLDS_BASE_URL` | ✅ | — | Your school's API base URL |\n| `LEARNWORLDS_API_TOKEN` | ✅ | — | Bearer access token |\n| `LEARNWORLDS_CLIENT_ID` | ✅ | — | Sent as the `Lw-Client` header |\n| `MCP_TRANSPORT` | — | `stdio` | `stdio` or `http` (the Docker image defaults to `http`) |\n| `PORT` | — | `8765` | HTTP listen port |\n| `HOST` | — | `0.0.0.0` | HTTP bind address |\n| `MCP_HTTP_PATH` | — | `/mcp` | HTTP MCP route |\n| `MCP_AUTH_TOKEN` | ⚠️ | _(off)_ | Require `Authorization: Bearer <token>` on `/mcp`. **Required** when `HOST` is not a loopback address, otherwise the server refuses to start. Renamed from `MCP_SHARED_TOKEN` in 1.1.0 |\n| `MCP_ALLOWED_HOSTS` | — | _(automatic)_ | Comma-separated hostnames accepted in the `Host` header (DNS-rebinding protection). Needed behind a reverse proxy |\n| `MCP_ALLOW_INSECURE` | — | _(off)_ | Lifts the startup refusal. Only for an endpoint nobody else can reach |\n| `MCP_SESSION_TTL` | — | `1800` | Idle seconds before a session is dropped |\n| `MCP_MAX_SESSIONS` | — | `256` | Maximum concurrent sessions |\n| `MCP_BODY_LIMIT` | — | `25mb` | Largest accepted request body |\n| `LEARNWORLDS_MAX_REQUESTS` | — | `25` | Client-side requests per window (`0` disables throttling) |\n| `LEARNWORLDS_RATE_WINDOW_MS` | — | `10000` | Rate-limit window in ms |\n| `LEARNWORLDS_MAX_RETRIES` | — | `3` | Retries on `429` / `5xx` / network errors |\n| `LEARNWORLDS_TIMEOUT_MS` | — | `30000` | Per-attempt request timeout |\n| `LEARNWORLDS_OPENAPI_PATH` | — | _(bundled)_ | Load a different OpenAPI YAML |\n\nAfter changing `.env`, reload with `docker compose up -d --force-recreate`.\n\n## Tool safety categories\n\nEach tool's description starts with one of these banners and carries the matching\n[MCP annotations](https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations):\n\n| Banner | Count | `readOnlyHint` | `destructiveHint` | Meaning |\n|---|:---:|:---:|:---:|---|\n| 🟢 **READ-ONLY** | 59 | `true` | `false` | Fetches data only. Safe. |\n| 🟡 **WRITE · creates data** | — | `false` | `false` | `POST` — creates records (not idempotent; may duplicate). |\n| 🟡 **WRITE · updates data** | — | `false` | `false` | `PUT` — modifies existing records in place (idempotent). |\n| 🔴 **DESTRUCTIVE · deletes** | 8 | `false` | `true` | `DELETE` — removes a record. Confirm first. |\n\nThe 🟡 write tools total **27** (17 create + 10 update). Hosts that respect annotations\n(Claude included) can require confirmation for `destructiveHint` tools and trust\n`readOnlyHint` tools automatically.\n\nUnder the banner, every description follows the same layout so an agent can act on the\nfirst read instead of probing:\n\n| Line | What it answers |\n|---|---|\n| *(purpose)* | What the endpoint does, straight from the OpenAPI spec, stated once. |\n| **Behavior** | What the call does to the live school, whether it is idempotent, and how auth, throttling and retries are handled for it. |\n| **Parameters** | Only what the input schema cannot say — the `body` wrapper, paging, and any renamed argument. |\n| **Returns** | The `HTTP <status>` + JSON result shape and the error codes that can come back. |\n| **Use when** | When to reach for it, when not to, and up to five sibling tools in the same area. |\n\n> Run `npm run list-tools` (no credentials needed) to print the full catalog and the\n> per-category counts at any time.\n\n<details>\n<summary><strong>Coverage by area (read / write / delete)</strong></summary>\n\n| Area | 🟢 Read | 🟡 Write | 🔴 Delete |\n|---|:---:|:---:|:---:|\n| Users | 10 | 7 | 1 |\n| Affiliates | 7 | 1 | 0 |\n| Community | 6 | 3 | 2 |\n| Courses | 5 | 3 | 0 |\n| Promotions (coupons) | 4 | 3 | 0 |\n| Reporting | 4 | 0 | 0 |\n| Payments | 3 | 0 | 0 |\n| Multiple seats | 3 | 3 | 2 |\n| User groups | 3 | 3 | 2 |\n| Bundles | 2 | 0 | 0 |\n| Assessments | 2 | 1 | 0 |\n| Subscription plans | 2 | 0 | 0 |\n| Certificates | 1 | 1 | 1 |\n| User subscriptions | 1 | 0 | 0 |\n| User roles | 1 | 0 | 0 |\n| Installments | 1 | 0 | 0 |\n| Leads | 1 | 0 | 0 |\n| Calendar | 1 | 0 | 0 |\n| Event logs | 1 | 0 | 0 |\n| Asynchronous actions | 1 | 0 | 0 |\n| Update user progress | 0 | 2 | 0 |\n| **Total** | **59** | **27** | **8** |\n</details>\n\n## Run from source (stdio, no Docker)\n\nPrefer the classic stdio mode for Claude Desktop? Build it locally:\n\n```bash\nnpm install\nnpm run build\n```\n\nThen point Claude Desktop at the compiled entrypoint in `claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"learnworlds\": {\n      \"command\": \"node\",\n      \"args\": [\"/ABSOLUTE/PATH/Learnworlds-MCP/dist/index.js\"],\n      \"env\": {\n        \"MCP_TRANSPORT\": \"stdio\",\n        \"LEARNWORLDS_BASE_URL\": \"https://your-school.learnworlds.com/admin/api\",\n        \"LEARNWORLDS_API_TOKEN\": \"your-access-token\",\n        \"LEARNWORLDS_CLIENT_ID\": \"your-client-id\"\n      }\n    }\n  }\n}\n```\n\n## Keeping the spec current\n\nThe bundled `spec/learnworlds-openapi.yaml` is the source of truth for the tools. To\nrefresh against a newer API version, drop the new YAML in its place (or point\n`LEARNWORLDS_OPENAPI_PATH` at it) and rebuild:\n\n```bash\ndocker compose up -d --build   # Docker\n# or\nnpm run build                  # from source\n```\n\nNew paths become new tools automatically — no code changes needed.\n\n## Development\n\n```bash\nnpm install\nnpm run build      # compile TypeScript → dist/\nnpm test           # run the Vitest suite\nnpm run list-tools # print the categorized tool catalog (no credentials needed)\n```\n\nCI builds and tests every push across Node 20 and 22; pushes to `main` also publish a\nDocker image to the GitHub Container Registry. When the version in `server.json` is one\nthe official MCP Registry does not yet list, that same run also publishes it there.\n\n## Notes & conventions\n\n- **Transports**: `MCP_TRANSPORT=stdio` (default) for local launchers; `MCP_TRANSPORT=http`\n  for the always-on Streamable-HTTP server the Docker image runs.\n- **Paging**: most `get` tools accept `page` (and endpoint-specific filters); LearnWorlds\n  paginates list responses (commonly 20–50 items per page).\n- **Rate limit**: LearnWorlds allows 30 requests / 10 s; the server self-throttles at\n  `LEARNWORLDS_MAX_REQUESTS` per `LEARNWORLDS_RATE_WINDOW_MS` (default 25 / 10 s) and\n  retries any `429` it still receives.\n- **Request bodies**: write tools take a `body` argument; its schema is resolved from the\n  spec and shown to the model.\n\n## Security\n\n- Your API credentials live only in `.env`, which is git-ignored. **Never commit real\n  secrets.** If the token leaks, rotate it in\n  **LearnWorlds admin → Settings → Integrations → Developers (API)**.\n- **The HTTP endpoint requires a token as soon as it is bound beyond loopback.**\n  Without `MCP_AUTH_TOKEN` the server refuses to start and the error explains what\n  to do. Send it as an `Authorization: Bearer <token>` header, ideally behind TLS.\n- **On localhost too:** without a token the `Host` header is restricted to\n  localhost names, so no web page can reach the endpoint via DNS rebinding. A\n  loopback bind alone is **not** protection. Behind a reverse proxy, set\n  `MCP_ALLOWED_HOSTS`.\n- **Behind a reverse proxy**, pin the `Host` header in the proxy to the internal\n  upstream name and put exactly that into `MCP_ALLOWED_HOSTS`. The check then does\n  not depend on the public domain and survives a domain change.\n  (Tip from [@WinFuture23](https://github.com/WinFuture23).)\n- **`/health` sits behind the Host check** but in front of the token check, so a\n  platform health check needs no token. It additionally accepts `localhost`,\n  `127.0.0.1` and `[::1]` at all times, so the `HEALTHCHECK` in the bundled\n  Dockerfile keeps working when you pin `MCP_ALLOWED_HOSTS` to a public\n  hostname. If your platform probes over HTTP from elsewhere, its hostname has\n  to be in the list: Railway sends `Host: healthcheck.railway.app`.\n\nSee [SECURITY.md](./SECURITY.md) for the full policy and how to report a vulnerability.\n\n## Credits & license\n\nAn unofficial community integration for [LearnWorlds](https://www.learnworlds.com/);\nnot affiliated with or endorsed by LearnWorlds. Built on the\n[Model Context Protocol](https://modelcontextprotocol.io). Licensed under the\n[MIT License](./LICENSE.md).\n"}