Декларации приложений

Плагин, чей MCP-сервер служит фронтом для десктопного приложения, объявляет, что это за приложение и что серверу от него нужно. Ядро оценивает декларацию на хосте и по результату открывает или закрывает инструменты сервера, а также любой навык, который называет это приложение. Парсер импортирует только стандартную библиотеку и hermes_platform.

Словарь определений лежит в hermes_platform/declaration.py. Декларация — это данные плюс политика; разбирается она из обычных отображений (уже декодированный YAML, JSON, литерал dict — сам парсер файлов не касается):

from hermes_platform import declaration

decl = declaration.parse_declaration(
    "my-server",
    raw_app={"linux": {"presence": "executable", "location": "/opt/my-app/server"}},
    raw_requires={"app": True},
    where="my-plugin/plugin.yaml",   # метка для людей, используется в сообщениях об ошибках
)
declaration.register("my-server", decl)

register(server_name, decl) сохраняет одну декларацию под настроенным именем сервера в реестре, локальном для процесса. Интеграция с загрузчиком — отдельная работа; ядро не читает YAML плагинов автоматически.

Для незарегистрированного MCP-сервера остаётся прежняя проверка только соединения. Навык, который явно называет незарегистрированный сервер, скрывается. clear() удаляет все регистрации и не служит операцией выгрузки отдельного плагина. Регистрации действуют на весь процесс, а не на профиль.

app — как найти приложение в каждой ОС

app:
  win32:
    presence: executable
    location: "%ProgramFiles%/Vendor/Vendor App/McpServer/Server.exe"
    version: { kind: uninstall_registry, display_name_prefix: "Vendor App" }
    liveness:
      kind: server_json
      path: "%LOCALAPPDATA%/Vendor/Vendor App/McpServer/server.json"
      pid_key: pid
      url_key: http
      token_key: token
      endpoint_path: /mcp
  darwin:
    presence: bundle
    location: /Applications/Vendor.app
    version: { kind: plist }

location — это один путь или упорядоченный список мест для поиска; побеждает первое найденное, но при requires.min_version читается версия каждой найденной копии, и побеждает первая подходящая. Элемент списка — путь либо словарь, называющий вид расположения:

app:
  win32:
    presence: executable
    location:
      - { kind: uninstall_registry, display_name_prefix: "Vendor App", file: vendor.exe }
      - "%ProgramFiles%/Vendor/Vendor App */vendor.exe"
    version: { kind: pe_resource }
  darwin:
    presence: bundle
    location: [{ kind: app_bundle, name: Vendor.app }]
    version: { kind: plist }
  linux:
    presence: executable
    location:
      - { kind: command, name: vendor }
      - { kind: flatpak, app_id: com.vendor.App }
      - { kind: snap, name: vendor }
вид расположенияОСключгде ищет
(строка с путём)любая—указанный путь; * заменяет папку с версией, сначала самая новая версия
commandлюбаяnamePATH (только для executable)
uninstall_registrywin32display_name_prefix, fileInstallLocation каждой подходящей записи об удалении, к которому добавляется file
app_bundledarwinname/Applications, затем ~/Applications (только для bundle)
flatpaklinuxapp_idсистемный, затем пользовательский flatpak exports/bin
snaplinuxname/snap/bin

ОС, ключ, тип присутствия и локатор каждого вида — это одна запись LOCATION_KINDS в hermes_platform/resolver/app.py; парсер проверяет объявления по этой таблице, так что новый вид — это одна запись там. Каталоги, на которые опирается каждый вид, лежат в hermes_platform/resolver/known_dirs.py.

полетипправилосоответствует в AppDef
<os>win32 | darwin | linuxхотя бы один; неизвестный ключ — ошибкаAppDef.os_family
presenceexecutable | bundleобязательно для каждой ОС.presence
locationstr | listобязательно; путь в Windows — от корня диска (C:\\...) либо начинается с ~ / %VAR% / $VAR; UNC-пути отклоняются, чтобы проверка наличия никогда не лезла в сеть; без сегментов .., ** и схемы URL; переменные раскрываются при поиске. Список содержит пути и словари видов расположения (см. выше).locations
version.kindpe_resource | plist | uninstall_registry | noneпо умолчанию none; pe_resource/uninstall_registry — только под win32, plist — только под darwin.version_kind
version.display_name_prefixstrобязательно при uninstall_registry.version_arg
liveness.kindserver_json | noneпо умолчанию none.liveness_kind
liveness.pathstrобязательно при server_json.liveness_path
liveness.pid_key / url_key / token_keystrпо умолчанию pid / http / token.liveness_*_key
liveness.endpoint_pathstrпо умолчанию /mcp; путь, по которому идёт initialize, — всегда этот, а не тот, что указан в файле.endpoint_path

Если requires.app равно true, ОС, которой нет в app:, получает unsupported_os.

requires — что нужно серверу, прежде чем его предложат

requires:
  app: true
  min_version: "2.3.0"
полетипправило
appboolпри true блок app: обязан существовать, а сервер предлагается только при наличии приложения
min_versionstrтребует app: true; числа через точку; каждый app.win32 и app.darwin обязан объявить настоящий version.kind; app.linux может его опустить (у Linux нет источника версии), и тогда проверяется только наличие, но объявление только для Linux минимум задать не может; сравнение числовое по сегментам, нечисловые символы внутри сегмента отбрасываются (2.3.0.12594 ≥ 2.3.0; суффиксы пререлизов не упорядочиваются)
gpustrnvidia; сервер предлагается только на хосте, где hermes_platform.host.facts.gpu_class() сообщает этого производителя. Не зависит от app: сервер без блока app: тоже может требовать GPU.

requires.app: true без блока app: — это DeclarationError.

gpu нужен приложению, у которого нет места установки для проверки или которому нужно железо независимо от того, что установлено. gpu_class() читает реестр в Windows, sysfs в Linux и архитектуру процессора в macOS, никогда не запуская подпроцесс и не обращаясь к библиотеке драйвера. Если GPU прочитать не удалось (unknown), требование считается выполненным: сбой чтения не должен закрывать доступ машине, на которой GPU есть, а проверка соединения всё равно остаётся. Принимается только nvidia: gpu_class() сообщает производителя с наивысшим приоритетом из имеющихся, так что на вопрос «есть ли здесь NVIDIA» он отвечает точно, а на тот же вопрос про AMD или Intel не ответил бы на машине, где есть ещё и NVIDIA.

Availability: единая оценка для всех читателей

hermes_platform/resolver/availability.py::availability(decl) -> Availability

Availability(
  state:   available | installed_not_running | missing_app | version_too_old
         | unsupported_os | unsupported_gpu | no_requirements,
  version: str | None,       # считанная версия, если есть
  path:    str | None,       # где приложение нашлось или где его искали
  min_version: str | None,   # из requires
)
  • no_requirements: requires.app нет, а requires.gpu (если задан) выполнено; гейт приложения пропускает, но проверка соединения по-прежнему действует.
  • unsupported_os: requires.app задан, а блока app.<эта ОС> нет. Никакого ввода-вывода.
  • unsupported_gpu: requires.gpu называет производителя, которого нет среди GPU этого хоста. Установка отклоняется (… is unavailable: unsupported_gpu, needs an NVIDIA GPU.), а строка статуса зарегистрированного сервера звучит так: «<app> needs an NVIDIA GPU; none was found on this machine. Use <app> on a machine with an NVIDIA GPU.»
  • missing_app: locate ничего не нашёл ни по одному из location.
  • version_too_old: версия ниже минимальной или её не удаётся прочитать.
  • available: приложение есть, версия подходит или не требуется.
  • installed_not_running: зарезервировано в словаре; этот вычислитель его никогда не выдаёт.

Оценка использует locate, необязательную проверку версии и кэшированный факт о GPU. Она никогда не опрашивает сервер, не запускает приложение и не подключается. Реестр инструментов сохраняет свой прежний кэш доступности.

Два гейта

  • MCP check_fn (tools/mcp_tool_handlers.py::_make_check_fn): соединение живо И, если для сервера зарегистрирована декларация с requires.app, availability(decl).offerable. Возвращает обычный bool, потому что реестр кэширует bool(fn()).
  • Frontmatter навыка requires_apps: (agent/skill_utils.py::skill_matches_apps): каждое имя разрешается через declaration.lookup; неизвестное имя скрывает навык (при сомнении — закрыто). Фильтр в момент предложения, как environments:.
перевод и редактура:

обновлено

⌕ ESC