mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-11 08:42:11 +00:00
fix(packaging): ship optional-mcps catalog in wheel and sdist (#39859)
The shipped MCP catalog (optional-mcps/) wasn't packaged, so `hermes mcp catalog` and the dashboard catalog screen come up empty on pip/Homebrew/Nix installs even though the manifests exist in the repo. The runtime expects a packaged catalog (get_optional_mcps_dir() -> _get_packaged_data_dir("optional-mcps"); list_catalog() returns [] when it's absent).
Ship it like locales: pyproject [tool.setuptools.data-files] for the wheel + a MANIFEST.in graft for the sdist. optional-mcps/ is nested (optional-mcps/<name>/manifest.yaml) and data-files flattens each glob into its target dir, so each catalog entry gets its own target to preserve the per-entry directory the catalog iterates over.
This commit is contained in:
parent
52f7e24a74
commit
39b76d9013
3 changed files with 52 additions and 0 deletions
|
|
@ -1,5 +1,6 @@
|
|||
graft skills
|
||||
graft optional-skills
|
||||
graft optional-mcps
|
||||
graft locales
|
||||
# Bundled plugin manifests (plugin.yaml / plugin.yml). Without these the
|
||||
# PluginManager scan (hermes_cli/plugins.py) finds zero plugins on installs
|
||||
|
|
|
|||
|
|
@ -285,6 +285,20 @@ py-modules = ["run_agent", "model_tools", "toolsets", "batch_runner", "trajector
|
|||
# venv) drop the catalogs and gateway/CLI commands surface raw i18n keys like
|
||||
# `gateway.reset.header_default` (#27632, #35374, #23943).
|
||||
locales = ["locales/*.yaml"]
|
||||
# Shipped MCP catalog (optional-mcps/<name>/manifest.yaml). Same bare-data-dir
|
||||
# case as locales: data-files ships it in the wheel, `graft optional-mcps` in
|
||||
# MANIFEST.in ships it in the sdist. Without this, `hermes mcp catalog` and the
|
||||
# dashboard catalog screen come up empty on packaged installs even though the
|
||||
# manifests exist in the repo (hermes_cli/mcp_catalog.py:_catalog_root resolves
|
||||
# the packaged dir; list_catalog() returns [] when it's missing).
|
||||
#
|
||||
# data-files flattens every glob match into its single target dir, so each
|
||||
# catalog entry needs its OWN target to preserve the per-entry directory the
|
||||
# catalog iterates over (a shared `optional-mcps/*/*` glob would collapse all
|
||||
# manifests into one colliding optional-mcps/manifest.yaml). One target per
|
||||
# entry; tests/test_packaging_metadata.py enforces an entry per optional-mcps/<name>.
|
||||
"optional-mcps/linear" = ["optional-mcps/linear/manifest.yaml"]
|
||||
"optional-mcps/n8n" = ["optional-mcps/n8n/manifest.yaml"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
hermes_cli = ["web_dist/**/*", "tui_dist/**/*", "scripts/install.sh", "scripts/install.ps1"]
|
||||
|
|
|
|||
|
|
@ -264,3 +264,40 @@ def test_locale_catalogs_ship_in_both_wheel_and_sdist():
|
|||
# Every on-disk catalog has the .yaml extension the globs above match.
|
||||
on_disk = list((REPO_ROOT / "locales").glob("*.yaml"))
|
||||
assert on_disk, "expected locales/*.yaml catalogs on disk"
|
||||
|
||||
|
||||
def test_optional_mcps_manifests_ship_in_both_wheel_and_sdist():
|
||||
"""Regression guard: the shipped MCP catalog must reach packaged installs.
|
||||
|
||||
hermes_cli/mcp_catalog.py resolves the catalog via get_optional_mcps_dir()
|
||||
-> _get_packaged_data_dir("optional-mcps"), and list_catalog() returns []
|
||||
when that directory is absent. optional-mcps/ is a bare data directory (no
|
||||
__init__.py), invisible to packages.find and package-data. It must ship as
|
||||
setuptools data-files (wheel) AND be grafted in MANIFEST.in (sdist), or
|
||||
`hermes mcp catalog` and the dashboard catalog screen come up empty on
|
||||
pip / Homebrew / Nix installs even though the manifests exist in the repo.
|
||||
|
||||
data-files flattens every glob match into its single target dir, so each
|
||||
catalog entry needs its OWN target to preserve the optional-mcps/<name>/
|
||||
directory the catalog iterates over. This asserts one target per on-disk
|
||||
entry so a newly-added MCP can't silently miss the wheel.
|
||||
"""
|
||||
entries = sorted(
|
||||
p.parent.name for p in (REPO_ROOT / "optional-mcps").glob("*/manifest.yaml")
|
||||
)
|
||||
assert entries, "expected optional-mcps/<name>/manifest.yaml on disk"
|
||||
|
||||
data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
||||
data_files = data["tool"]["setuptools"].get("data-files", {})
|
||||
for name in entries:
|
||||
target = f"optional-mcps/{name}"
|
||||
assert target in data_files, (
|
||||
f"pyproject [tool.setuptools.data-files] must declare a '{target}' "
|
||||
f"target so the wheel ships optional-mcps/{name}/manifest.yaml "
|
||||
f"(data-files flattens globs, so each catalog entry needs its own target)"
|
||||
)
|
||||
|
||||
manifest = (REPO_ROOT / "MANIFEST.in").read_text(encoding="utf-8")
|
||||
assert "graft optional-mcps" in manifest, (
|
||||
"MANIFEST.in must `graft optional-mcps` so the sdist ships MCP manifests"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue