itstep-mystat-mcp 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. itstep_mystat_mcp-0.1.1/.env.example +14 -0
  2. itstep_mystat_mcp-0.1.1/.github/workflows/publish.yml +46 -0
  3. itstep_mystat_mcp-0.1.1/.gitignore +48 -0
  4. itstep_mystat_mcp-0.1.1/PKG-INFO +15 -0
  5. itstep_mystat_mcp-0.1.1/README.md +235 -0
  6. itstep_mystat_mcp-0.1.1/docs/api-findings.md +138 -0
  7. itstep_mystat_mcp-0.1.1/mystat/__init__.py +3 -0
  8. itstep_mystat_mcp-0.1.1/mystat/__main__.py +10 -0
  9. itstep_mystat_mcp-0.1.1/mystat/api.py +195 -0
  10. itstep_mystat_mcp-0.1.1/mystat/attachments.py +369 -0
  11. itstep_mystat_mcp-0.1.1/mystat/auth.py +233 -0
  12. itstep_mystat_mcp-0.1.1/mystat/cache.py +449 -0
  13. itstep_mystat_mcp-0.1.1/mystat/cli.py +454 -0
  14. itstep_mystat_mcp-0.1.1/mystat/clients.py +486 -0
  15. itstep_mystat_mcp-0.1.1/mystat/config.py +214 -0
  16. itstep_mystat_mcp-0.1.1/mystat/extraction.py +182 -0
  17. itstep_mystat_mcp-0.1.1/mystat/homework.py +579 -0
  18. itstep_mystat_mcp-0.1.1/mystat/search.py +47 -0
  19. itstep_mystat_mcp-0.1.1/mystat/server.py +224 -0
  20. itstep_mystat_mcp-0.1.1/pyproject.toml +41 -0
  21. itstep_mystat_mcp-0.1.1/scripts/discover.py +286 -0
  22. itstep_mystat_mcp-0.1.1/scripts/discover_details.py +181 -0
  23. itstep_mystat_mcp-0.1.1/scripts/discover_files.py +89 -0
  24. itstep_mystat_mcp-0.1.1/scripts/probe_auth_email.py +35 -0
  25. itstep_mystat_mcp-0.1.1/scripts/probe_auth_extra.py +52 -0
  26. itstep_mystat_mcp-0.1.1/scripts/probe_auth_shape.py +60 -0
  27. itstep_mystat_mcp-0.1.1/scripts/probe_bundle.py +42 -0
  28. itstep_mystat_mcp-0.1.1/scripts/probe_login.py +50 -0
  29. itstep_mystat_mcp-0.1.1/scripts/probe_web_client.py +53 -0
  30. itstep_mystat_mcp-0.1.1/server.py +10 -0
  31. itstep_mystat_mcp-0.1.1/uv.lock +1274 -0
@@ -0,0 +1,14 @@
1
+ MYSTAT_LOGIN=your_login
2
+ MYSTAT_PASSWORD=your_password
3
+
4
+ MYSTAT_BASE_URL=https://mapi.itstep.org/v1/mystat
5
+ MYSTAT_LANGUAGE=uk
6
+ MYSTAT_STATUS_FILTER=3
7
+ MYSTAT_SORT=-hw.time
8
+
9
+ CACHE_DIR=./cache
10
+ DATABASE_PATH=./cache/mystat.db
11
+
12
+ SYNC_LIMIT=1000
13
+ HTTP_TIMEOUT=30
14
+ MAX_ATTACHMENT_SIZE_MB=100
@@ -0,0 +1,46 @@
1
+ name: publish
2
+
3
+ # Trusted Publishing (OIDC): no API token is stored in the repository.
4
+ # PyPI must be configured with:
5
+ # owner = stealbe
6
+ # repository = itstep-mystat-mcp
7
+ # workflow = publish.yml
8
+ # environment= pypi
9
+ on:
10
+ release:
11
+ types: [published]
12
+ workflow_dispatch:
13
+
14
+ jobs:
15
+ build:
16
+ name: build sdist and wheel
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: astral-sh/setup-uv@v7
21
+ with:
22
+ python-version: "3.13"
23
+ - name: Build
24
+ run: uv build
25
+ - uses: actions/upload-artifact@v4
26
+ with:
27
+ name: dist
28
+ path: dist/
29
+ if-no-files-found: error
30
+
31
+ publish:
32
+ name: publish to PyPI
33
+ needs: build
34
+ runs-on: ubuntu-latest
35
+ # Must match the "Environment name" of the pending publisher on PyPI.
36
+ environment: pypi
37
+ permissions:
38
+ # Required for OIDC: without this the action cannot obtain a token.
39
+ id-token: write
40
+ steps:
41
+ - uses: actions/download-artifact@v4
42
+ with:
43
+ name: dist
44
+ path: dist/
45
+ - name: Publish
46
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,48 @@
1
+ # ---- secrets -------------------------------------------------------------
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ .token
6
+ .token.*
7
+
8
+ # ---- local state and caches ---------------------------------------------
9
+ cache/
10
+ logs/
11
+ *.log
12
+ *.db
13
+ *.db-wal
14
+ *.db-shm
15
+ *.part
16
+
17
+ # ---- python --------------------------------------------------------------
18
+ __pycache__/
19
+ *.py[cod]
20
+ *.egg-info/
21
+ build/
22
+ dist/
23
+ .pytest_cache/
24
+ .mypy_cache/
25
+ .ruff_cache/
26
+ .coverage
27
+ coverage.xml
28
+ htmlcov/
29
+
30
+ # ---- virtual environments ------------------------------------------------
31
+ .venv/
32
+ .venv*/
33
+ venv/
34
+
35
+ # ---- editors and OS ------------------------------------------------------
36
+ .idea/
37
+ .vscode/
38
+ *.swp
39
+ .DS_Store
40
+ Thumbs.db
41
+ desktop.ini
42
+
43
+ # ---- leftovers -----------------------------------------------------------
44
+ uv.lock.bak
45
+ .tmp-*/
46
+
47
+ # ---- local test suite (kept on disk, not versioned) ----------------------
48
+ tests/
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.5
2
+ Name: itstep-mystat-mcp
3
+ Version: 0.1.1
4
+ Summary: Local MCP server exposing ITStep MyStat homework (list, details, attachments, search)
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: aiosqlite>=0.20
7
+ Requires-Dist: beautifulsoup4>=4.12
8
+ Requires-Dist: httpx>=0.27
9
+ Requires-Dist: mcp>=1.2
10
+ Requires-Dist: openpyxl>=3.1
11
+ Requires-Dist: pillow>=10.0
12
+ Requires-Dist: pypdf>=5.0
13
+ Requires-Dist: python-docx>=1.1
14
+ Requires-Dist: python-dotenv>=1.0
15
+ Requires-Dist: python-pptx>=1.0
@@ -0,0 +1,235 @@
1
+ # itstep-mystat-mcp
2
+
3
+ Локальный MCP-сервер (stdio) для доступа к домашним заданиям ITStep MyStat:
4
+ синхронизация списка заданий, загрузка вложений, извлечение текста, поиск.
5
+
6
+ Ставится одной командой `uv tool install`, настраивается мастером `setup`:
7
+ логин и пароль вводятся в терминале, сервер сам прописывается в локальные
8
+ клиенты (OpenCode, Claude Code, Claude Desktop, Codex).
9
+
10
+ ## Быстрый старт
11
+
12
+ ```powershell
13
+ uv tool install itstep-mystat-mcp # из индекса (когда опубликован)
14
+ # или из локальной папки проекта:
15
+ uv tool install .
16
+
17
+ itstep-mystat-mcp setup # логин/пароль + авто-регистрация в клиентах
18
+ ```
19
+
20
+ `setup` задаёт три вопроса (если значения не переданы флагами) и делает всё сам:
21
+
22
+ 1. спрашивает логин MyStat (`MYSTAT_LOGIN`);
23
+ 2. спрашивает пароль (ввод скрыт, просит повторить);
24
+ 3. проверяет пару реальным `POST /auth/login`;
25
+ 4. пишет конфиг в `%APPDATA%\itstep-mystat-mcp\.env`;
26
+ 5. регистрирует MCP-сервер в найденных клиентах.
27
+
28
+ После этого достаточно перезапустить клиент.
29
+
30
+ ## Что умеет
31
+
32
+ | Инструмент | Назначение |
33
+ |---|---|
34
+ | `mystat_login` | принудительный login, замена сохранённого токена |
35
+ | `mystat_auth_status` | есть ли токен, когда получен, известен ли срок (токен не раскрывается) |
36
+ | `mystat_logout` | удалить токен из локального кеша |
37
+ | `homework_sync` | синхронизация списка + деталей + вложений |
38
+ | `homework_list` | список заданий из кеша (краткие сведения) |
39
+ | `homework_get` | одно задание: текст, вложения, статусы |
40
+ | `homework_search` | полнотекстовый поиск (FTS5) |
41
+ | `homework_read_file` | чтение вложения или извлечённого текста |
42
+ | `homework_cache_status` | статистика кеша |
43
+
44
+ ## Команды CLI
45
+
46
+ ```powershell
47
+ itstep-mystat-mcp setup # мастер настройки (логин/пароль + клиенты)
48
+ itstep-mystat-mcp run # MCP-сервер по stdio (это вызывают клиенты)
49
+ itstep-mystat-mcp status # пути конфига/кеша, наличие кредов, токен, размер кеша
50
+ itstep-mystat-mcp sync # разовая синхронизация из терминала
51
+ itstep-mystat-mcp login # принудительный login, токен в кеш
52
+ itstep-mystat-mcp logout # удалить токен
53
+ ```
54
+
55
+ `itstep-mystat-mcp run` без аргументов — поведение по умолчанию (так его
56
+ запускают MCP-клиенты).
57
+
58
+ ### Флаги `setup`
59
+
60
+ | Флаг | Смысл |
61
+ |---|---|
62
+ | `--login <login>` | логин без интерактивного вопроса |
63
+ | `--env-file <path>` | куда писать конфиг (по умолчанию `%APPDATA%\itstep-mystat-mcp\.env`) |
64
+ | `--cache-dir <path>` | каталог кеша (по умолчанию рядом с конфигом) |
65
+ | `--client auto\|all\|none\|<name>` | целевые клиенты; можно повторять. `auto` — найденные на машине (по умолчанию) |
66
+ | `--verify` / `--no-verify` | проверять ли креды реальным login (по умолчанию проверяет) |
67
+ | `-y`, `--yes` | не задавать вопросов: брать значения из флагов, `.env` или окружения |
68
+
69
+ Пароль можно передать через переменную окружения `MYSTAT_PASSWORD` — тогда
70
+ мастер не спрашивает его в интерактиве (удобно для скриптов и CI):
71
+
72
+ ```powershell
73
+ $env:MYSTAT_PASSWORD = "..." # останется в истории шелла — учитывайте это
74
+ itstep-mystat-mcp setup --login my_login --client none --no-verify
75
+ ```
76
+
77
+ ### Куда ставится конфиг и клиенты
78
+
79
+ | Клиент | Путь / способ |
80
+ |---|---|
81
+ | конфиг сервера | `%APPDATA%\itstep-mystat-mcp\.env` (кеш — `%APPDATA%\itstep-mystat-mcp\cache`) |
82
+ | OpenCode | `~/.config/opencode/opencode.jsonc` (или `opencode.json`) |
83
+ | Claude Code | `claude mcp add --scope user` (используется CLI, если он в `PATH`) |
84
+ | Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` |
85
+ | Codex | `~/.codex/config.toml` (`$CODEX_HOME` учитывается) |
86
+
87
+ Перед изменением существующего файла создаётся резервная копия
88
+ `<имя>.bak-YYYYMMDD-HHMMSS`. Если файл не парсится, он **не** перезаписывается —
89
+ вместо этого выводится сниппет для ручной вставки.
90
+
91
+ ## Ручное подключение (если авто-регистрация не сработала)
92
+
93
+ OpenCode, `~/.config/opencode/opencode.jsonc`:
94
+
95
+ ```jsonc
96
+ "mcp": {
97
+ "itstep-mystat": {
98
+ "type": "local",
99
+ "command": ["C:\\Users\\<you>\\AppData\\Local\\uv\\tools\\itstep-mystat-mcp\\Scripts\\python.exe", "-m", "mystat"],
100
+ "environment": { "PYTHONUTF8": "1" },
101
+ "enabled": true,
102
+ "timeout": 180000
103
+ }
104
+ }
105
+ ```
106
+
107
+ Claude Desktop / общий JSON-формат:
108
+
109
+ ```json
110
+ {
111
+ "mcpServers": {
112
+ "itstep-mystat": {
113
+ "command": "itstep-mystat-mcp",
114
+ "args": ["run"],
115
+ "env": { "PYTHONUTF8": "1" }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ Codex, `~/.codex/config.toml`:
122
+
123
+ ```toml
124
+ [mcp_servers.itstep-mystat]
125
+ command = "itstep-mystat-mcp"
126
+ args = ["run"]
127
+ env = { PYTHONUTF8 = "1" }
128
+ ```
129
+
130
+ Таймаут увеличен: первая синхронизация скачивает десятки файлов.
131
+
132
+ ## Разработка (из исходников)
133
+
134
+ ```powershell
135
+ uv sync
136
+ uv run python -m mystat status
137
+ uv run python -m mystat sync --no-attachments
138
+ ```
139
+
140
+ `python server.py` оставлен как тонкая обёртка над `mystat.server:main`.
141
+
142
+ Сервер общается по stdio. `stdout` занят протоколом; все логи идут в `stderr`
143
+ и в `cache/logs/mystat.log`.
144
+
145
+ ## Структура
146
+
147
+ ```text
148
+ server.py тонкая обёртка (совместимость)
149
+ mystat/__main__.py python -m mystat
150
+ mystat/cli.py setup / run / status / sync / login / logout
151
+ mystat/clients.py регистрация MCP в opencode, claude*, codex
152
+ mystat/server.py MCP-сервер (mcp 2.x MCPServer, stdio, 9 инструментов)
153
+ mystat/config.py .env, пути, лимиты
154
+ mystat/cache.py SQLite (WAL), миграции, FTS5
155
+ mystat/auth.py login / refresh / хранение токена
156
+ mystat/api.py единый HTTP-клиент, 401-recovery, ретраи
157
+ mystat/homework.py нормализация, sync, read model
158
+ mystat/attachments.py валидация URL, скачивание, имена, MIME
159
+ mystat/extraction.py PDF/DOCX/PPTX/XLSX/TXT/HTML
160
+ mystat/search.py безопасные FTS5-запросы
161
+ docs/api-findings.md подтверждённый контракт API
162
+ scripts/ разведочные скрипты (не входят в поставку)
163
+ ```
164
+
165
+ ## Ключевые решения (по фактам API)
166
+
167
+ - Токен — JWT, живёт 30 дней; `expires_at` берётся из `exp`.
168
+ - `401` → `POST /auth/refresh` с текущим токеном; при неудаче — `POST /auth/login`.
169
+ Один повтор запроса, не более.
170
+ - Текст задания есть **только** в `GET /od/homework/{id}` (`comment` в списке пуст).
171
+ - Детали приходят объектом с ключом `"0"`, а не массивом — нормализатор
172
+ поддерживает оба варианта.
173
+ - Имя файла — из `Content-Disposition` (`filename*`, UTF-8); MIME проверяется
174
+ по magic bytes; `ETag` у файлового хранилища недостоверен, поэтому изменения
175
+ определяются по SHA-256.
176
+ - Скачиваются только `https://*.itstep.org/api/v1/files/...`; проверяется
177
+ разрешённый IP (private/loopback/link-local запрещены), размер ограничен,
178
+ редиректы проверяются.
179
+ - ZIP не распаковывается, OCR не выполняется (первая версия).
180
+
181
+ ## Безопасность
182
+
183
+ - Логин и пароль хранятся только в локальном конфиге, который не попадает
184
+ в репозиторий (`.env` в `.gitignore`).
185
+ - Пароль никогда не выводится: `setup` читает его скрытым вводом, `status`
186
+ показывает только `configured` / `MISSING`.
187
+ - Токен никогда не возвращается MCP-инструментами и не пишется в логи;
188
+ заголовок `Authorization` в лог не попадает.
189
+ - Регистрация клиентов не удаляет чужие записи: изменяется только ключ
190
+ `itstep-mystat`.
191
+
192
+ ## Тесты
193
+
194
+ Набор тестов есть локально, но не входит в репозиторий (см. `.gitignore`):
195
+
196
+ ```powershell
197
+ .venv\Scripts\python.exe -m pytest -q
198
+ ```
199
+
200
+ 82 теста: авторизация и refresh, ретраи, нормализация, sync, пометка
201
+ исчезнувших заданий, безопасность URL и имён, скачивание, FTS5, CLI,
202
+ регистрация клиентов и кодирование значений `.env`. Тесты не ходят в сеть:
203
+ `httpx.MockTransport` и обезличенные фикстуры в `tests/fixtures/`.
204
+
205
+ ## Формат `.env`
206
+
207
+ `setup` пишет значение без кавычек, когда это безопасно (логин, Windows-пути),
208
+ и в двойных кавычках с экранированием — когда внутри есть пробелы, `#`,
209
+ кавычки или обрамляющие пробелы:
210
+
211
+ ```dotenv
212
+ MYSTAT_LOGIN=student_login
213
+ MYSTAT_PASSWORD="pa ss#1 \"x\""
214
+ CACHE_DIR=C:\Users\you\AppData\Roaming\itstep-mystat-mcp\cache
215
+ ```
216
+
217
+ Читается файл с `interpolate=False`: `python-dotenv` подставляет `${VAR}`
218
+ даже внутри кавычек, и без этого пароль вида `${...}` молча превращался бы в
219
+ пустую строку. После записи значения сверяются обратным чтением — при
220
+ расхождении `setup` сообщает об ошибке, не печатая сами значения.
221
+
222
+ `--cache-dir` всегда сохраняется абсолютным: MCP-клиент запускает сервер из
223
+ произвольного рабочего каталога, относительный путь разрешился бы не там.
224
+
225
+ ## Ограничения
226
+
227
+ - Первый запуск `homework_sync` требует действующих учётных данных
228
+ (мастер `setup` или `.env`).
229
+ - Токен хранится в SQLite открытым текстом (как в ТЗ). Шифрование через
230
+ Windows DPAPI можно добавить отдельным этапом.
231
+ - Если пароль не принимается, API отвечает одинаковым `401` и для неверного
232
+ пароля, и для несуществующего логина — поэтому «неверный пароль» и
233
+ «неизвестный логин» неразличимы.
234
+ - Проектный `.env` в папке разработки имеет приоритет над
235
+ `%APPDATA%\itstep-mystat-mcp\.env`; `setup` предупреждает об этом.
@@ -0,0 +1,138 @@
1
+ # MyStat API — подтверждённые факты (discovery)
2
+
3
+ **Дата:** 12.11.2026
4
+ **Источник:** реальные запросы к `https://mapi.itstep.org/v1/mystat` + JS-бандл веб-клиента `https://mystat.itstep.org/main-PTSNAXV7.js`.
5
+ **Статус:** всё ниже — наблюдённые факты, кроме явно помеченных INFERENCE.
6
+
7
+ Закрывает неизвестные из раздела 18 ТЗ.
8
+
9
+ ---
10
+
11
+ ## 1. Авторизация
12
+
13
+ **`POST /auth/login`**
14
+
15
+ ```http
16
+ Content-Type: application/json
17
+ X-Language: uk
18
+
19
+ {"login": "<login>", "password": "<password>"}
20
+ ```
21
+
22
+ - `200` → тело **plain text JWT** (не JSON). `Content-Type: text/html; charset=UTF-8`, длина 604.
23
+ - `401` → `Unauthorized: error_unauthorized` (одинаково для неверного логина и пароля).
24
+ - `422` → `Validation error. The Password is required` (без пароля) / `validation_custom_email_required_without` (без логина).
25
+ - Имена полей: только `login` + `password`. `username`, `email` не принимаются (`500`/`422`).
26
+ - `X-Language`, `Origin`, `Referer`, `User-Agent`, `X-Userid` на результат **не влияют**.
27
+
28
+ **JWT claims (payload):** `iss, iat, exp, nbf, jti, sub, id, id_user_storage, email, username, name, branch_id, project_id, lang, profile, module, branch_alias`.
29
+ `exp - iat = 2 592 000 c` = **30 дней** → `expires_at` можно заполнять из токена без верификации подписи.
30
+
31
+ **`POST /auth/refresh`** (в ТЗ помечен как неизвестный — теперь известно)
32
+
33
+ ```http
34
+ Authorization: Bearer <access_token>
35
+ Content-Type: application/json
36
+
37
+ {}
38
+ ```
39
+
40
+ - `200` → новый JWT тем же plain-text форматом. **Refresh token не нужен** — сам access token выступает основанием для refresh.
41
+ - `401` без заголовка → `Unauthorized: Token is empty`.
42
+
43
+ Веб-клиент после логина вызывает `/auth/me` (`accountService.getAccountForce$`), но для MCP-потока он не требуется: `list` и `homework/{id}` работают сразу с Bearer-токеном.
44
+
45
+ ---
46
+
47
+ ## 2. Список заданий
48
+
49
+ **`GET /od/homework/list?sort=-hw.time&status=3&limit=1000`**
50
+
51
+ Заголовки: `Authorization: Bearer <token>`, `X-Language: uk`. **`X-Userid` не требуется** (проверено: без него `200`).
52
+
53
+ Форма ответа:
54
+
55
+ ```json
56
+ { "data": [ ... 60 элементов ... ], "_meta": { "currentPage": 1, "totalPages": 1, "totalCount": 60 } }
57
+ ```
58
+
59
+ Поля элемента списка:
60
+
61
+ | Поле | Тип | Пример / смысл |
62
+ |---|---|---|
63
+ | `id` | int | `502719` — ID задания |
64
+ | `id_spec` | int | ID предмета |
65
+ | `id_teach` | int | ID преподавателя |
66
+ | `id_group` | int | ID группы |
67
+ | `fio_teach` | str | ФИО преподавателя |
68
+ | `theme` | str | тема задания |
69
+ | `name_spec` | str | название предмета |
70
+ | `comment` | str | **всегда пустой в списке** (см. п.3) |
71
+ | `creation_time` / `completion_time` / `overdue_time` | str | `YYYY-MM-DD` |
72
+ | `file_path` | str \| null | URL основного вложения |
73
+ | `cover_image` | str \| null | URL картинки-обложки |
74
+ | `status` | int | наблюдалось только `3` при фильтре `status=3` |
75
+ | `homework_stud` | obj | данные сдачи: `id, file_path, mark, creation_time, stud_answer, auto_mark, status, photo_pas, fio_stud, related_homeworks, related_materials, unlock_expire, communication_history, is_retake` |
76
+ | `homework_comment` | obj | `text_comment, attachment, attachment_path, date_updated` |
77
+ | `is_autotest`, `autotest_status`, `autotest_result`, `ospr`, `default_deadline_days`, `coins_to_close_deadline`, `completion_days`, `is_academic_holiday` | mixed | служебные |
78
+
79
+ Заголовков `X-Pagination-*` сервер **не отдаёт** — пагинация только через `_meta`.
80
+
81
+ ---
82
+
83
+ ## 3. Детали задания
84
+
85
+ **`GET /od/homework/{id}`**
86
+
87
+ Форма ответа — **не массив**, а объект с ключом-строкой `"0"`:
88
+
89
+ ```json
90
+ { "0": { ...поля задания... }, "relatedHomeworks": [ ... ] }
91
+ ```
92
+
93
+ Парсер обязан корректно принимать: объект с ключом `"0"`, массив из одного элемента и (на всякий случай) одиночный объект.
94
+
95
+ Поля внутри `"0"` совпадают с полями списка, **но `comment` здесь содержит реальный текст задания** (проверено на 10 заданиях: в списке пусто, в деталях 9–539 символов). Значит `homework_sync` обязан запрашивать детали — иначе текст задания теряется.
96
+
97
+ `relatedHomeworks[]`: `id_domzadstud, id_teach, id_stud, status, time, id_domzad, ospr, mark, nlenta, date_vizit, is_retake, auto_test_status, auto_test_response, unlock_expire, moved_from_inactive_teacher, created_at, updated_at, domZad{ id_domzad, material_link, id_teach, id_tgroups, id_spec, time, id_schedule, nlenta, date, ospr, dz_theme, deadline, is_autotest, public_material_uuid, spec{...} }`.
98
+
99
+ `updated_at` в `relatedHomeworks` — единственный кандидат на «время изменения», но относится к связанным сдачам, не к самому заданию. **Для самого задания поля изменения нет** → определение изменений по SHA-256 нормализованного JSON деталей (как в ТЗ).
100
+
101
+ ---
102
+
103
+ ## 4. Вложения
104
+
105
+ В JSON **нет** имени, MIME и размера файла — только URL.
106
+
107
+ | Поле | Смысл |
108
+ |---|---|
109
+ | `file_path` | основное вложение задания (есть у 53 из 60 заданий) |
110
+ | `cover_image` | картинка-обложка задания (не вложение) |
111
+ | `homework_stud.file_path` | файл, загруженный студентом (в наблюдённых данных `null`) |
112
+ | `homework_comment.attachment` / `attachment_path` | вложение к комментарию преподавателя |
113
+
114
+ Наблюдённые домены: `fs1.itstep.org`, `fs3.itstep.org`, `fsx1.itstep.org`.
115
+ Шаблон пути: `/api/v1/files/<opaque-token>`.
116
+
117
+ Скачивание **открытое, без авторизации**:
118
+
119
+ - `200`, `access-control-allow-origin: *`, `server: nginx`;
120
+ - `Content-Disposition: attachment; filename="DZ_2026_11_09.pdf"; filename*=utf-8''ДЗ_2026_11_09.pdf` → отсюда берём исходное имя (декодировать `filename*` по RFC 5987);
121
+ - `Content-Type` и `Content-Length` корректны (проверено: `application/pdf`, `...wordprocessingml.document`, `video/mp4`, `image/png`, `text/html`);
122
+ - `Last-Modified` присутствует не всегда;
123
+ - **`ETag` непригоден**: у разных файлов одинаковый sha1 пустой строки. Условные запросы по ETag использовать нельзя → только SHA-256 содержимого.
124
+
125
+ Вложение может быть **HTML-документом** (сгенерированная страница задания `Домашнє_завдання_...html`) — извлечение текста обязано это поддерживать.
126
+
127
+ ---
128
+
129
+ ## 5. Следствия для реализации
130
+
131
+ 1. Токен хранить как plain text JWT + `expires_at` из `exp` (30 дней).
132
+ 2. На `401`: сначала `POST /auth/refresh` с Bearer, при неудаче — `POST /auth/login`; один повтор исходного запроса.
133
+ 3. `homework_sync` всегда тянет `homework/{id}` (в списке нет текста задания).
134
+ 4. Allowlist файлов: HTTPS + host заканчивается на `.itstep.org` + path начинается с `/api/v1/files/`.
135
+ 5. Имя файла: `Content-Disposition` → `filename*` (UTF-8) → `filename` → имя из URL → `attachment_<n>`.
136
+ 6. MIME: доверять `Content-Type`, но проверять magic bytes (PK\x03\x04, %PDF, \x89PNG, ftyp, <!DOCTYPE).
137
+ 7. Change detection: SHA-256 нормализованного JSON деталей и SHA-256 содержимого файла; ETag игнорировать.
138
+ 8. `status=3` — единственное наблюдённое значение; вынести в конфиг.
@@ -0,0 +1,3 @@
1
+ """itstep-mystat-mcp --- MyStat homework access for MCP clients."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,10 @@
1
+ """Allows ``python -m mystat`` (used by MCP client configs)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())