Декларации приложений
Плагин, чей 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 | любая | name | PATH (только для executable) |
uninstall_registry | win32 | display_name_prefix, file | InstallLocation каждой подходящей записи об удалении, к которому добавляется file |
app_bundle | darwin | name | /Applications, затем ~/Applications (только для bundle) |
flatpak | linux | app_id | системный, затем пользовательский flatpak exports/bin |
snap | linux | name | /snap/bin |
ОС, ключ, тип присутствия и локатор каждого вида — это одна запись LOCATION_KINDS в hermes_platform/resolver/app.py; парсер проверяет объявления по этой таблице, так что новый вид — это одна запись там. Каталоги, на которые опирается каждый вид, лежат в hermes_platform/resolver/known_dirs.py.
| поле | тип | правило | соответствует в AppDef |
|---|---|---|---|
<os> | win32 | darwin | linux | хотя бы один; неизвестный ключ — ошибка | AppDef.os_family |
presence | executable | bundle | обязательно для каждой ОС | .presence |
location | str | list | обязательно; путь в Windows — от корня диска (C:\\...) либо начинается с ~ / %VAR% / $VAR; UNC-пути отклоняются, чтобы проверка наличия никогда не лезла в сеть; без сегментов .., ** и схемы URL; переменные раскрываются при поиске. Список содержит пути и словари видов расположения (см. выше) | .locations |
version.kind | pe_resource | plist | uninstall_registry | none | по умолчанию none; pe_resource/uninstall_registry — только под win32, plist — только под darwin | .version_kind |
version.display_name_prefix | str | обязательно при uninstall_registry | .version_arg |
liveness.kind | server_json | none | по умолчанию none | .liveness_kind |
liveness.path | str | обязательно при server_json | .liveness_path |
liveness.pid_key / url_key / token_key | str | по умолчанию pid / http / token | .liveness_*_key |
liveness.endpoint_path | str | по умолчанию /mcp; путь, по которому идёт initialize, — всегда этот, а не тот, что указан в файле | .endpoint_path |
Если requires.app равно true, ОС, которой нет в app:, получает unsupported_os.
requires — что нужно серверу, прежде чем его предложат
requires:
app: true
min_version: "2.3.0"
| поле | тип | правило |
|---|---|---|
app | bool | при true блок app: обязан существовать, а сервер предлагается только при наличии приложения |
min_version | str | требует app: true; числа через точку; каждый app.win32 и app.darwin обязан объявить настоящий version.kind; app.linux может его опустить (у Linux нет источника версии), и тогда проверяется только наличие, но объявление только для Linux минимум задать не может; сравнение числовое по сегментам, нечисловые символы внутри сегмента отбрасываются (2.3.0.12594 ≥ 2.3.0; суффиксы пререлизов не упорядочиваются) |
gpu | str | nvidia; сервер предлагается только на хосте, где 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:.