# MCP: описание работы, структуры и тулзов Документ описывает MCP endpoint сервиса `miem-employees` по текущей реализации в `app/mcp.py`. ## Где находится MCP - FastAPI router: `app.mcp.router` - Подключение к приложению: `app/main.py` - HTTP endpoint: `POST /mcp` - Локально при обычном запуске API: `http://localhost:8000/mcp` - В Docker Compose через отдельный сервис `mcp`: `http://localhost:8001/mcp` - Авторизация на уровне приложения: отсутствует. Заголовок `Authorization` не проверяется и не влияет на ответ. Если доступ к MCP нужно ограничить, это должно делаться внешним контуром: bind на localhost, VPN, firewall, reverse proxy или отдельная сетевая политика. ## Протокол Endpoint принимает JSON-RPC 2.0 over HTTP. Общий формат запроса: ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} } ``` Общий формат успешного ответа: ```json { "jsonrpc": "2.0", "id": 1, "result": {} } ``` Общий формат ошибки: ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32601, "message": "Method not found" } } ``` Поддерживаемая версия MCP-протокола: ```text 2024-11-05 ``` Имя сервиса: ```text miem-employees ``` Версия сервера берется из `app.version.BACKEND_VERSION`. ## Поддерживаемые JSON-RPC методы ### initialize Возвращает метаданные MCP-сервера и capabilities. Запрос: ```json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} } ``` Ответ: ```json { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "serverInfo": { "name": "miem-employees", "version": "0.5.0" }, "capabilities": { "tools": {} } } } ``` ### tools/list Возвращает список доступных tools с JSON Schema для аргументов. Запрос: ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} } ``` Ответ содержит массив `result.tools`. ### tools/call Вызывает один tool по имени. Запрос: ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "search_employees", "arguments": { "query": "Сергеев", "limit": 20 } } } ``` Ответ tool всегда заворачивается в MCP content-массив: ```json { "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "{\"items\":[]}" } ] } } ``` Поле `text` содержит сериализованный JSON с `ensure_ascii=false`. Клиент должен распарсить это поле как JSON, если ему нужна структурированная нагрузка. ## Ошибки - Неизвестный JSON-RPC метод: `code = -32601`, `message = "Method not found"`. - Исключения при обработке tool: `code = -32000`, `message` содержит текст исключения. - Если сущность не найдена внутри отдельных tools, HTTP и JSON-RPC ответ остаются успешными, а полезная нагрузка содержит `{"error": "not_found"}`. ## Источники данных MCP читает данные из основной базы через SQLAlchemy session из `app.db.get_db`. Основные таблицы и модели: - `employees`: текущая карточка сотрудника, статус, профиль, `current_data`, checksum. - `employee_publications`: нормализованные публикации сотрудников с авторами, DOI, аннотацией, описанием, citation text и raw JSON из HSE Publications. - `crawl_runs`: история запусков парсинга. - `crawl_run_employee_changes`: детальные изменения сотрудников в рамках запуска. - `crawl_errors`: ошибки парсинга в рамках запуска. - `dataset_versions`: версии полного набора сотрудников. - `dataset_version_items`: состав конкретной версии набора сотрудников. ## Общая структура employee payload Краткая карточка сотрудника: ```json { "profile_key": "staff:avsergeev", "profile_id": "avsergeev", "full_name": "Сергеев Алексей Викторович", "status": "active", "canonical_url": "https://www.hse.ru/staff/avsergeev", "last_seen_at": "2026-05-14T10:00:00+00:00", "dismissed_at": null } ``` В sync payload дополнительно отдается `checksum`. Полная карточка дополнительно содержит: ```json { "data": { "contacts": {}, "sections": [] } } ``` `data` соответствует распарсенному JSON профиля сотрудника. Внутри `sections` могут быть секции с публикациями, курсами, ВКР, таблицами, ссылками и произвольными текстовыми блоками. ## Tools ### get_service_info Назначение: вернуть метаданные сервиса, список tools и текущую версию набора сотрудников. Аргументы: отсутствуют. Возвращает: ```json { "service_name": "miem-employees", "backend_version": "0.5.0", "protocolVersion": "2024-11-05", "tools": [], "dataset": { "hash": "sha256", "previous_hash": "sha256 или null", "created_at": "2026-05-14T10:00:00+00:00", "crawl_run_id": 123, "employee_count": 100, "active_count": 95, "dismissed_count": 5 } } ``` Особенность: перед ответом сервис создает актуальную `dataset_version`, если текущий набор сотрудников еще не имеет версии. ### sync_employees Назначение: синхронизировать клиентский кэш сотрудников по hash набора данных. Аргументы: ```json { "client_hash": "sha256 или null", "include_data": true } ``` - `client_hash`: hash версии, которая уже есть у клиента. Если не передан, отдается полный snapshot. - `include_data`: управляет включением полного `data` в карточки сотрудников. По умолчанию `true`. Полный ответ без `client_hash`: ```json { "mode": "full", "from_hash": null, "to_hash": "current-sha256", "dataset": {}, "items": [] } ``` Если клиентский hash совпадает с текущим: ```json { "mode": "delta", "from_hash": "current-sha256", "to_hash": "current-sha256", "dataset": {}, "changes": { "added": [], "updated": [], "dismissed": [], "removed": [] } } ``` Если `client_hash` неизвестен серверу: ```json { "mode": "full", "from_hash": "missing", "to_hash": "current-sha256", "dataset": {}, "items": [], "reason": "unknown_client_hash" } ``` Если `client_hash` найден и отличается от текущего: ```json { "mode": "delta", "from_hash": "old-sha256", "to_hash": "current-sha256", "dataset": {}, "changes": { "added": [], "updated": [], "dismissed": [], "removed": [] } } ``` Логика delta: - `added`: сотрудник появился в новой версии. - `updated`: изменился checksum или статус, и сотрудник активен. - `dismissed`: сотрудник есть в новой версии, но получил статус `dismissed`. - `removed`: `profile_key` был в старой версии, но отсутствует в новой. Hash набора считается по отсортированному списку `{profile_key, status, checksum}`. ### search_employees Назначение: найти сотрудников по ФИО или canonical URL. Аргументы: ```json { "query": "Сергеев", "status": "active", "limit": 20 } ``` - `query`: обязательный по schema, но в коде пустая строка означает поиск без текстового фильтра. - `status`: опционально, только `active` или `dismissed`. - `limit`: максимум 100, по умолчанию 20. Возвращает массив кратких employee payload без `data`: ```json [ { "profile_key": "staff:avsergeev", "profile_id": "avsergeev", "full_name": "Сергеев Алексей Викторович", "status": "active", "canonical_url": "https://www.hse.ru/staff/avsergeev", "last_seen_at": "2026-05-14T10:00:00+00:00", "dismissed_at": null } ] ``` ### get_employee Назначение: получить одну карточку сотрудника. Аргументы: ```json { "profile_id_or_url": "avsergeev" } ``` Поиск выполняется по: - `profile_key` - `profile_id` - точному `canonical_url` - частичному совпадению `canonical_url` Возвращает полный employee payload с `data`. Если сотрудник не найден: ```json { "error": "not_found" } ``` ### list_employee_publications Назначение: вернуть публикации сотрудника. Если есть нормализованные строки в `employee_publications`, tool возвращает детальные публикационные данные: авторов, DOI, аннотацию, описание, citation text, год, тип, язык, статус и ссылки. Если детальная таблица еще не заполнена, tool использует старый fallback из `employees.current_data.sections[].publications`. Аргументы: ```json { "profile_id_or_url": "avsergeev" } ``` Поиск сотрудника выполняется так же, как в `get_employee`: по `profile_key`, `profile_id`, точному или частичному `canonical_url`. Порядок источников: - сначала `employee_publications`, отсортированные по году, названию и внутреннему id; - если записей нет, секции `current_data.sections` с `type = "publications"` и массивами `publications`. Ответ: ```json { "employee": { "profile_key": "org_person:803294906", "profile_id": "803294906", "full_name": "Борисов Сергей Петрович", "status": "active", "canonical_url": "https://www.hse.ru/org/persons/803294906", "last_seen_at": "2026-05-14T10:00:00+00:00", "dismissed_at": null }, "items": [ { "id": "888959076", "publication_id": "888959076", "title": "Название публикации", "text": "Краткое описание или citation", "url": "https://publications.hse.ru/view/888959076", "year": 2023, "type": "ARTICLE", "publication_type": "ARTICLE", "language": "ru", "status": 1, "doi_url": "https://doi.org/10.53921/18195822_2023_23_4_624", "other_url": "https://example.test", "document_url": "https://example.test/file.pdf", "citation_text": "Авторы. Название публикации // Журнал. 2023.", "annotation": { "ru": "Аннотация", "en": "Abstract" }, "description": { "main": "Авторы. Название публикации // Журнал. 2023." }, "authors": [ { "id": "803294906", "href": "https://www.hse.ru/org/persons/803294906", "title_ru": "Борисов С. П.", "title_en": "", "reverse_title_ru": "С. П. Борисов", "reverse_title_en": "", "alt_name": "S. P. Borisov", "other_name": null, "is_current_employee": true } ] } ] } ``` В fallback-режиме из `current_data` старые элементы могут содержать только базовые поля `title`, `text`, `url` и `id`. Если сотрудник не найден: ```json { "items": [] } ``` Если сотрудник найден, но публикаций нет: ```json { "employee": {}, "items": [] } ``` ### list_employee_courses Назначение: вернуть курсы преподавания сотрудника из распарсенных секций профиля. Аргументы: ```json { "profile_id_or_url": "avsergeev" } ``` Сервис ищет секции `current_data.sections` с `type = "courses_by_year"` и объединяет массивы `courses`. Ответ: ```json { "employee": {}, "items": [ { "title": "Название курса", "url": "https://..." } ] } ``` Если сотрудник или данные профиля отсутствуют: ```json { "items": [] } ``` ### get_crawl_status Назначение: вернуть последний запуск парсинга. Аргументы: отсутствуют. Ответ: ```json { "id": 123, "status": "completed", "source_url": "https://miem.hse.ru/persons", "started_at": "2026-05-14T10:00:00+00:00", "finished_at": "2026-05-14T10:10:00+00:00", "found_count": 100, "parsed_count": 98, "error_count": 2, "dismissed_count": 1 } ``` Если запусков еще не было: ```json { "status": "never_run" } ``` ### get_crawl_run_details Назначение: вернуть детальную информацию по конкретному запуску парсинга: summary, изменения сотрудников и ошибки. Аргументы: ```json { "run_id": 123 } ``` Ответ: ```json { "id": 123, "source_url": "https://miem.hse.ru/persons", "status": "completed", "status_display": "Завершен", "started_at": "2026-05-14T10:00:00+00:00", "finished_at": "2026-05-14T10:10:00+00:00", "started_display": "14.05.2026 13:00", "finished_display": "14.05.2026 13:10", "found_count": 100, "parsed_count": 98, "new_count": 3, "error_count": 2, "dismissed_count": 1, "processed_count": 100, "progress_percent": 100.0, "message": null, "changes_detail_available": true, "changes": { "new": [], "missing_from_source": [], "dismissed": [] }, "errors": [] } ``` Если запуск не найден: ```json { "error": "not_found" } ``` ## Примеры curl Список tools: ```bash curl http://localhost:8001/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' ``` Поиск сотрудника: ```bash curl http://localhost:8001/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_employees","arguments":{"query":"Сергеев","limit":5}}}' ``` Полная синхронизация: ```bash curl http://localhost:8001/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"sync_employees","arguments":{"include_data":false}}}' ``` Delta-синхронизация: ```bash curl http://localhost:8001/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"sync_employees","arguments":{"client_hash":"known-sha256","include_data":true}}}' ``` ## Как MCP используется клиентом 1. Клиент вызывает `initialize` и проверяет `protocolVersion`. 2. Клиент вызывает `tools/list`, чтобы получить актуальный список tools и input schemas. 3. Для поиска и точечных запросов клиент вызывает `tools/call` с `search_employees`, `get_employee`, `list_employee_publications`, `list_employee_courses`, `get_crawl_status` или `get_crawl_run_details`. 4. Для локального кэша клиент вызывает `get_service_info` или `sync_employees`. 5. Клиент хранит последний `dataset.hash`. 6. При следующей синхронизации клиент передает hash как `client_hash`. 7. Сервер возвращает пустую delta, delta с изменениями или полный snapshot, если hash неизвестен. ## Важные особенности реализации - MCP endpoint read-only: tools не запускают парсинг и не меняют сотрудников напрямую. - `get_service_info` и `sync_employees` могут создать новую запись `dataset_versions`, если состояние сотрудников изменилось и новой версии еще нет. - Все tool payloads возвращаются как JSON-строка внутри `content[0].text`. - `search_employees` ищет через `ilike` по `full_name` и `canonical_url`. - `get_employee` допускает частичный URL, поэтому строка `133709486` может найти `https://www.hse.ru/org/persons/133709486`. - Временные значения сериализуются через `isoformat()`, display-поля для админских payload формируются в часовом поясе `Europe/Moscow`.