stepik-mcp
by prog420
README.md
# stepik-mcp
[](https://github.com/prog420/stepik-mcp/actions/workflows/ci.yml)
[](https://www.python.org/downloads/)
[](LICENSE)
An [MCP](https://modelcontextprotocol.io) server that lets an AI assistant author courses on
[Stepik](https://stepik.org): browse the catalogue, read a course's structure, and create or
edit courses, modules, lessons and steps through the [Stepik REST API](https://stepik.org/api/docs).
**It cannot publish.** Courses it creates stay private drafts and no tool can make one public —
that stays a human decision in the Stepik web interface. See [Safety](#safety) below.
## Requirements
- Python 3.10+
- Stepik OAuth application credentials (free)
## Getting credentials
1. Go to <https://stepik.org/oauth2/applications/> and register a new application.
2. Set **Client type** to `Confidential` and **Authorization grant type** to `Client credentials`.
3. Copy the **Client id** and **Client secret**.
The token issued by this grant belongs to the account that owns the application, so everything
this server creates is owned by that account. Run `stepik_whoami` to confirm which one it is.
## Install
With [uv](https://docs.astral.sh/uv/) (no manual install needed — `uvx` fetches on demand):
```bash
uvx --from git+https://github.com/prog420/stepik-mcp stepik-mcp
```
Or from a clone:
```bash
git clone https://github.com/prog420/stepik-mcp
cd stepik-mcp
uv sync --extra dev
uv run stepik-mcp
```
## Configuration
All configuration is via environment variables.
| Variable | Default | Meaning |
| --- | --- | --- |
| `STEPIK_CLIENT_ID` | — | **Required.** OAuth application client id. |
| `STEPIK_CLIENT_SECRET` | — | **Required.** OAuth application client secret. |
| `STEPIK_ALLOW_PUBLISH` | `0` | Set to `1` to allow visibility changes. See [Safety](#safety). |
| `STEPIK_ALLOW_DELETE` | `1` | Set to `0` to make the server read-and-create only. |
| `STEPIK_BASE_URL` | `https://stepik.org` | Override for a self-hosted instance. |
| `STEPIK_TIMEOUT` | `30` | HTTP timeout in seconds. |
| `STEPIK_MAX_PAGES` | `5` | Page cap when following paginated listings. |
A `.env` file in the working directory is loaded automatically; copy `.env.example` to start.
### Claude Code
```bash
claude mcp add stepik \
--env STEPIK_CLIENT_ID=your-id \
--env STEPIK_CLIENT_SECRET=your-secret \
-- uvx --from git+https://github.com/prog420/stepik-mcp stepik-mcp
```
### Claude Desktop / Cursor
In `claude_desktop_config.json` or `.cursor/mcp.json`:
```json
{
"mcpServers": {
"stepik": {
"command": "uvx",
"args": ["--from", "git+https://github.com/prog420/stepik-mcp", "stepik-mcp"],
"env": {
"STEPIK_CLIENT_ID": "your-id",
"STEPIK_CLIENT_SECRET": "your-secret"
}
}
}
}
```
### Remote / containerised
```bash
stepik-mcp --transport http --host 0.0.0.0 --port 8000
# -> streamable HTTP at http://<host>:8000/mcp
```
The HTTP transport has no authentication of its own. Do not expose it to a network you do not
control — anyone who can reach it can act as your Stepik account.
## Content model
Stepik's hierarchy is worth knowing, because one part of it is not obvious:
```
course
└── section (a module)
└── unit (a placement, not content)
└── lesson (exists independently of any course!)
└── step (theory or a quiz)
```
A **lesson is a standalone object**, not a child of a course. Placing one into a course means
creating a **unit** that points at a section and a lesson. `stepik_add_lesson_to_course` does the
lesson + unit + steps sequence in a single call, which is the normal way to add content.
Steps are edited through **step-sources** (the authoring object), not `steps` (the read-only
learner view).
## Tools
### Reading
| Tool | Purpose |
| --- | --- |
| `stepik_whoami` | Which Stepik account the server acts as. |
| `stepik_health_check` | Credentials, API reachability, and which safety switches are on. |
| `stepik_search_courses` | Free-text search of the public catalogue. |
| `stepik_list_courses` | List courses; with no arguments, the ones you own. |
| `stepik_get_course` | One course's metadata. |
| `stepik_get_course_structure` | **The whole tree** — sections, units, lessons, steps, with every id. |
| `stepik_get_lesson` | One lesson and its step ids. |
| `stepik_list_steps` | Steps of a lesson with short previews. |
| `stepik_get_step_source` | A step's full editable content, including quiz options and answers. |
### Authoring
| Tool | Purpose |
| --- | --- |
| `stepik_create_course` | Create a course. Always a private draft. |
| `stepik_update_course` | Edit title, summary, description, workload, audience, requirements. |
| `stepik_create_section` / `stepik_update_section` | Manage modules. |
| `stepik_create_lesson` / `stepik_update_lesson` | Manage standalone lessons. |
| `stepik_create_unit` / `stepik_move_unit` | Place and reorder lessons within a course. |
| `stepik_create_step` / `stepik_update_step` | Manage step content of any type. |
| `stepik_add_lesson_to_course` | **Composite:** lesson + unit + steps in one call. |
| `stepik_delete_section` / `_lesson` / `_unit` / `_step` | Deletion; each requires `confirm=true`. |
Positions are optional everywhere — omit `position` and the new object is appended to the end.
### Step types
`stepik_create_step` covers every type through one `block_type` argument:
| `block_type` | Extra arguments |
| --- | --- |
| `text` | none — a theory/reading step |
| `choice` | `options` (≥2, ≥1 with `is_correct`); single vs multiple answer is inferred |
| `sorting` | `options` in the correct order |
| `matching` | `pairs` (≥2) of `first`/`second` |
| `string` | `answer`; optional case sensitivity, substring and regex matching |
| `number` | `answer` (numeric), optional `max_error` |
| `math` | `answer` as an expression |
| `free-answer` | none — reviewed manually |
| `code` | `code_tests` (input/output), optional `code_template`, `code_language` |
Invalid combinations are rejected before the request is sent, with a message that says what is
missing rather than a bare HTTP 400.
### Resources
- `stepik://me` — the authenticated account
- `stepik://course/{course_id}` — a markdown outline of a course
## Safety
The point of this server is that an assistant can build a course without being able to ship it.
**Publishing is blocked in the client, not in the tools.** Removing a `publish` tool alone would
be theatre: an agent could still call `update_course(is_enabled=true)`. Instead, every write
passes through one chokepoint in [`client.py`](src/stepik_mcp/client.py) that strips or rejects
fields which would widen visibility (`is_enabled`, `is_public`, `is_featured`). Consequences:
- Created courses are forced to `is_enabled: false`.
- Created lessons are forced to `is_public: false`.
- An update that would publish raises an error and **no request reaches Stepik**.
- Editing a course that is *already* published still works — only *widening* visibility is blocked,
and unpublishing is always allowed.
- `STEPIK_ALLOW_PUBLISH=1` lifts the block. It is an operator decision made when starting the
server, not something a tool call can reach.
This is covered by [`tests/test_publish_guard.py`](tests/test_publish_guard.py); if those tests
fail, the guarantee is gone.
**Deletion** is gated twice: `STEPIK_ALLOW_DELETE` must be on (it is by default) *and* each delete
call must pass `confirm=true`. Delete tools are marked with the MCP `destructiveHint` annotation
so clients can prompt. Deleting a lesson or step also destroys learner submissions, and Stepik has
no undo.
## Development
```bash
uv sync --extra dev
uv run pytest # 86 tests, no network access
uv run ruff check .
uv run ruff format .
```
Tests mock the HTTP layer with [respx](https://lundberg.github.io/respx/), so the suite never
touches stepik.org and needs no credentials.
To inspect the server interactively:
```bash
npx @modelcontextprotocol/inspector uv run stepik-mcp
```
## Notes on the Stepik API
Things that surprised us, recorded so the next person does not have to rediscover them:
- Every GET returns a **list**, even for a single id: `/api/courses/1` yields `{"courses": [...]}`.
A missing object comes back as an empty list with HTTP 200, not a 404.
- The schema is **flat** — there are no nested endpoints like `/courses/1/sections`. Use
`/sections?course=1` or the `?ids[]=` batch form, which every endpoint supports.
- `/api/stepics/1` ("who am I") is a **trap**. The `1` means "me", but the `stepics` object it
returns also has `id: 1`, so trusting that id resolves to user #1 — the site's very first
account — for everyone. The real profile is the sibling `users` array of the same response.
- Request bodies are wrapped in a singular envelope: `{"course": {...}}`. Step sources are the
exception and use camelCase `{"stepSource": {...}}`; this client sends that and retries with
`step-source` if Stepik rejects it.
- `PUT` replaces the whole object, so all updates here are read-modify-write. Sending only the
changed field would blank out everything else.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing