Files
miem_workers/MCP_DESCRIPTION.md

18 KiB
Raw Permalink Blame History

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.

Общий формат запроса:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}

Общий формат успешного ответа:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {}
}

Общий формат ошибки:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32601,
    "message": "Method not found"
  }
}

Поддерживаемая версия MCP-протокола:

2024-11-05

Имя сервиса:

miem-employees

Версия сервера берется из app.version.BACKEND_VERSION.

Поддерживаемые JSON-RPC методы

initialize

Возвращает метаданные MCP-сервера и capabilities.

Запрос:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {}
}

Ответ:

{
  "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 для аргументов.

Запрос:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}

Ответ содержит массив result.tools.

tools/call

Вызывает один tool по имени.

Запрос:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_employees",
    "arguments": {
      "query": "Сергеев",
      "limit": 20
    }
  }
}

Ответ tool всегда заворачивается в MCP content-массив:

{
  "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

Краткая карточка сотрудника:

{
  "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.

Полная карточка дополнительно содержит:

{
  "data": {
    "contacts": {},
    "sections": []
  }
}

data соответствует распарсенному JSON профиля сотрудника. Внутри sections могут быть секции с публикациями, курсами, ВКР, таблицами, ссылками и произвольными текстовыми блоками.

Tools

get_service_info

Назначение: вернуть метаданные сервиса, список tools и текущую версию набора сотрудников.

Аргументы: отсутствуют.

Возвращает:

{
  "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 набора данных.

Аргументы:

{
  "client_hash": "sha256 или null",
  "include_data": true
}
  • client_hash: hash версии, которая уже есть у клиента. Если не передан, отдается полный snapshot.
  • include_data: управляет включением полного data в карточки сотрудников. По умолчанию true.

Полный ответ без client_hash:

{
  "mode": "full",
  "from_hash": null,
  "to_hash": "current-sha256",
  "dataset": {},
  "items": []
}

Если клиентский hash совпадает с текущим:

{
  "mode": "delta",
  "from_hash": "current-sha256",
  "to_hash": "current-sha256",
  "dataset": {},
  "changes": {
    "added": [],
    "updated": [],
    "dismissed": [],
    "removed": []
  }
}

Если client_hash неизвестен серверу:

{
  "mode": "full",
  "from_hash": "missing",
  "to_hash": "current-sha256",
  "dataset": {},
  "items": [],
  "reason": "unknown_client_hash"
}

Если client_hash найден и отличается от текущего:

{
  "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.

Аргументы:

{
  "query": "Сергеев",
  "status": "active",
  "limit": 20
}
  • query: обязательный по schema, но в коде пустая строка означает поиск без текстового фильтра.
  • status: опционально, только active или dismissed.
  • limit: максимум 100, по умолчанию 20.

Возвращает массив кратких employee payload без data:

[
  {
    "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

Назначение: получить одну карточку сотрудника.

Аргументы:

{
  "profile_id_or_url": "avsergeev"
}

Поиск выполняется по:

  • profile_key
  • profile_id
  • точному canonical_url
  • частичному совпадению canonical_url

Возвращает полный employee payload с data.

Если сотрудник не найден:

{
  "error": "not_found"
}

list_employee_publications

Назначение: вернуть публикации сотрудника. Если есть нормализованные строки в employee_publications, tool возвращает детальные публикационные данные: авторов, DOI, аннотацию, описание, citation text, год, тип, язык, статус и ссылки. Если детальная таблица еще не заполнена, tool использует старый fallback из employees.current_data.sections[].publications.

Аргументы:

{
  "profile_id_or_url": "avsergeev"
}

Поиск сотрудника выполняется так же, как в get_employee: по profile_key, profile_id, точному или частичному canonical_url.

Порядок источников:

  • сначала employee_publications, отсортированные по году, названию и внутреннему id;
  • если записей нет, секции current_data.sections с type = "publications" и массивами publications.

Ответ:

{
  "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.

Если сотрудник не найден:

{
  "items": []
}

Если сотрудник найден, но публикаций нет:

{
  "employee": {},
  "items": []
}

list_employee_courses

Назначение: вернуть курсы преподавания сотрудника из распарсенных секций профиля.

Аргументы:

{
  "profile_id_or_url": "avsergeev"
}

Сервис ищет секции current_data.sections с type = "courses_by_year" и объединяет массивы courses.

Ответ:

{
  "employee": {},
  "items": [
    {
      "title": "Название курса",
      "url": "https://..."
    }
  ]
}

Если сотрудник или данные профиля отсутствуют:

{
  "items": []
}

get_crawl_status

Назначение: вернуть последний запуск парсинга.

Аргументы: отсутствуют.

Ответ:

{
  "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
}

Если запусков еще не было:

{
  "status": "never_run"
}

get_crawl_run_details

Назначение: вернуть детальную информацию по конкретному запуску парсинга: summary, изменения сотрудников и ошибки.

Аргументы:

{
  "run_id": 123
}

Ответ:

{
  "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": []
}

Если запуск не найден:

{
  "error": "not_found"
}

Примеры curl

Список tools:

curl http://localhost:8001/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Поиск сотрудника:

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}}}'

Полная синхронизация:

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-синхронизация:

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.