hermes-agent: plugin vs skill, and packaging hermes-rss as native integration
Researched against the locally installed hermes-agent v0.20.0 at
~/.hermes/hermes-agent (confirmed via pyproject.toml version = "0.20.0"
and ~/.hermes/.update_check), plus the public GitHub repo, the
0xNyk/awesome-hermes-agent list, and the hosted docs at
hermes-agent.nousresearch.com. All local file paths below are read-only
inspections of the installed copy at ~/.hermes/hermes-agent.
Plugin vs skill (what each actually is in this codebase)
Skill = a directory of markdown + optional scripts, loaded by the agent's
own reasoning as procedural knowledge to read and follow. There is no
Python code that the host imports — a skill is prompt content plus,
optionally, shell scripts the agent decides to run via the terminal tool.
This is exactly what ~/.hermes/skills/science-recommendations/SKILL.md
already is: instructions telling the model "curl this local FastAPI server,
here's the shape of the response." Skills are self-improving in this
project's design — the README describes "autonomous skill creation after
complex tasks" and skills "self-improve during use" (agent-curated, not
developer-curated).
Plugin = an installed Python package (bundled, user, project, or pip
entry-point) that the host process imports and that calls back into
host-provided registration APIs. A plugin is code, not prose. Concretely,
each plugin directory (e.g. ~/.hermes/hermes-agent/plugins/spotify/)
contains:
plugin.yaml— manifest (name,version,description,author,kind,provides_tools,hooks,requires_env)__init__.py— must expose aregister(ctx)function- arbitrary supporting
.pymodules (tools.py,client.py, ...)
register(ctx) receives a PluginContext (defined in
~/.hermes/hermes-agent/hermes_cli/plugins.py) that exposes ~15 registration
methods: register_tool(), register_hook(), register_middleware(),
register_cli_command(), register_command() (in-chat slash commands),
register_context_engine(), register_image_gen_provider(),
register_web_search_provider(), register_browser_provider(),
register_tts_provider(), register_transcription_provider(),
register_secret_source(), register_dashboard_auth_provider(),
register_video_gen_provider(), register_platform(),
register_slack_action_handler(), register_auxiliary_task(),
register_skill() (yes — a plugin can register its own namespaced skill,
<plugin_name>:<skill_name>, that stays out of the flat skills tree).
The practical distinction that matters for us: a skill tells the model
what to do (imperative prose the agent interprets each turn, at the cost of
context-window tokens and interpretation risk — e.g. our SKILL.md's
documented reasoning_effort footgun). A plugin gives the host a
structured, typed, function-calling tool schema (provides_tools +
SPOTIFY_PLAYBACK_SCHEMA-style JSON Schema dicts, see
plugins/spotify/tools.py lines 328-454) that's injected into the model's
tool-calling surface exactly like a built-in tool — no prose interpretation,
no curl-command hallucination risk, and it can run outside a single
turn (hooks, background lifecycle).
Extension points found (with file paths / line refs in the local install)
All paths are under ~/.hermes/hermes-agent/ unless noted.
- Plugin manifest schema —
hermes_cli/plugins.pylines 280-314 (PluginManifestdataclass). Validkindvalues (line 277):standalone,backend,exclusive,platform,model-provider. - Plugin loader / discovery order —
hermes_cli/plugins.pyPluginManager._discover_and_load_inner(), lines 1336-1391. Four sources, later overrides earlier on name collision: - Bundled:
<repo>/plugins/<name>/ - User:
~/.hermes/plugins/<name>/— this is where our engine's plugin would go - Project:
./.hermes/plugins/<name>/(opt-in viaHERMES_ENABLE_PROJECT_PLUGINS) - Pip: packages exposing the
hermes_agent.pluginsentry-point group - Opt-in gating — standalone/user-installed plugins are gated by
plugins.enabled: [...]inconfig.yaml(lines 1469-1477 ofplugins.py). Our locally installed~/.hermes/config.yamlcurrently has noplugins:key at all — confirmed by direct read, so zero third-party plugins are enabled today; only bundledbackend/platformkinds auto-load. - Lifecycle hooks (
VALID_HOOKS) —hermes_cli/plugins.pylines 135-215. Full set:pre_tool_call,post_tool_call,transform_terminal_output,transform_tool_result,transform_llm_output,pre_llm_call,post_llm_call,pre_verify,pre_api_request,post_api_request,api_request_error,on_session_start,on_session_end,on_session_finalize,on_session_reset,subagent_start,subagent_stop,pre_gateway_dispatch,pre_approval_request,post_approval_response,kanban_task_claimed,kanban_task_completed,kanban_task_blocked. Real example:plugins/disk-cleanup/plugin.yamldeclareshooks: [post_tool_call, on_session_end]and needs no agent action — this is the "background job" pattern most relevant to a ranking engine that wants to react to session/tool events without being explicitly invoked. - Tool registration —
PluginContext.register_tool(),hermes_cli/plugins.pylines 410-465, delegates totools.registry.registry.register()(tools/registry.py, not read in full — referenced fromplugins/spotify/tools.pyline 17). Confirmed real usage:plugins/spotify/tools.pydefines 7 tool schemas (SPOTIFY_PLAYBACK_SCHEMAetc., lines 328-454) each a plain JSON-Schema{"name", "description", "parameters"}dict — i.e. exactly OpenAI/ Anthropic-style function-calling schemas, not curl instructions. - CLI discovery commands —
hermes plugins list,hermes plugins install owner/repo, or "drop a plugin directory into~/.hermes/plugins/" (message text found atcli.pyline 10212, in the/pluginsslash-command handler). - No formal Python base class for a "Plugin" — registration is
duck-typed via the
register(ctx)entry point; provider-style extensions (image gen, web search, browser, TTS, STT) do have ABCs (ImageGenProvider,WebSearchProvider,BrowserProvider,TTSProvider,TranscriptionProvider,SecretSource— all imported fromagent.*_providermodules inplugins.py), but a plain function-calling tool plugin likespotifyneeds none of that — it's justregister_tool()calls.
MCP support (yes — first-class, and organizationally separate from plugins)
Yes, hermes-agent has native MCP client support, and it is a third, distinct extension mechanism alongside skills and plugins, not a special case of either:
- Config lives under a top-level
mcp_servers:key in~/.hermes/config.yaml(confirmed absent in our current config — no MCP servers configured locally right now; the key/mechanism exists in the codebase regardless, perhermes_cli/agent_import.pylines 24-29, 481-804 which importmcpServersfrom Claude Code / Codex config formats into this same key). - CLI:
hermes mcp add <name> --command <cmd> --args ... --env KEY=VALUEfor stdio-transport local servers, or--urlfor HTTP/SSE remote servers, or--preset <name>for known integrations. Full arg parser read athermes_cli/subcommands/mcp.pylines 44-73 — confirms--command(destmcp_command, "Stdio command (e.g. npx)"),--args(nargs=REMAINDER),--env(nargs="*",KEY=VALUEpairs),--auth {oauth,header},--connect-timeout. UNVERIFIED (from hosted docs, not the local install): the exactmcp_servers.<name>YAML key names (command,args,env,timeout,connect_timeout,idle_timeout_seconds,max_lifetime_seconds) — the docs fetch returned these but I did not find amcp_servers:block in any locally-loaded config to cross-check field names directly. Treat key names as probably-right, verify againsthermes mcp add --helpoutput before committing config. - There is also a real bundled MCP catalog —
~/.hermes/hermes-agent/optional-mcps/<name>/manifest.yaml, e.g.optional-mcps/linear/manifest.yaml(read in full):manifest_version: 1,transport: {type: http, url: ...},auth: {type: oauth},post_install:free text. This is a curated list of Nous-approved remote MCP servers users one-click install viahermes mcp install <name>/hermes mcp catalog/hermes mcp picker— not a mechanism for registering our own local server (we'd usehermes mcp add, not the catalog, since we're not submitting a PR to hermes-agent's repo). - hermes-agent's own
pyproject.tomlpins the official MCP Python SDK:mcp = ["mcp==1.28.1", "starlette==1.3.1"](grep-confirmed, lines 233-251) — used both for its MCP client (connecting out to servers like ours) and forhermes mcp serve(exposing Hermes itself as an MCP server to other agents — the reverse direction, not relevant here). - The
mcpPyPI package (same official SDK,FastMCPincluded) is NOT currently a dependency of/home/matt/hermes-rss— confirmed viapython3 -c "import mcp"failing locally and nomcpentry in hermes-rss'spyproject.toml. It would need to be added.
Bottom line: MCP is the right "native integration" lane for us, not the
hermes-agent plugin system. A hermes-agent plugin requires our code to
live inside ~/.hermes/plugins/ as a hermes-agent-specific Python module
that imports hermes_cli.plugins, tools.registry, etc. — i.e. it couples
hermes-rss to hermes-agent's internal APIs and versions. An MCP server is a
standard, host-agnostic protocol boundary: hermes-rss stays a fully
independent FastAPI process/package, and hermes-agent (or Claude Desktop, or
anything else MCP-capable) just connects to it over stdio or HTTP. Given
hermes-rss is already a standalone FastAPI service at 127.0.0.1:8899 with a
CLI, MCP is strictly less invasive than a plugin for the same "native
function-calling tool" benefit.
Recommended packaging path for our engine
Wrap hermes-rss as an MCP stdio server (not a plugin, not the HTTP/OAuth catalog path — we don't need remote transport or OAuth for a loopback personal tool).
Concrete plan:
- New file:
/home/matt/hermes-rss/src/hermes_rss/mcp_server.pyusingmcp.server.fastmcp.FastMCP(part of themcpSDK, samemcp==1.28.1version hermes-agent itself pins — match it to avoid a protocol-version mismatch). Define 3-4@mcp.tool()-decorated async functions that call directly into the existing engine internals (not over HTTP-to-self — import the ranking/feedback functions from thehermes-rsspackage directly, since both processes would otherwise be redundant): list_recommendations(user: str, limit: int = 10) -> list[dict]mark_feedback(item_id: str, useful: bool) -> dictexplain_ranking(item_id: str) -> strReuse the same request/response shapes the FastAPI routes already use (checksrc/for the existing route handlers — likelysrc/hermes/api.pyor similar, not yet inspected in this pass) so the MCP tool layer is a thin adapter, not a reimplementation. If those functions currently assume an already-running FastAPI app/DB connection, the MCP server module should perform the same engine-init/DB-open sequencescripts/setup.shdoes today, or simplyimport httpxand proxy to the already-running127.0.0.1:8899instance if in-process reuse turns out messy — either is legitimate; in-process is preferred to avoid running two servers.- New CLI entry point in
pyproject.toml:hermes-rss-mcp = "hermes_rss.mcp_server:main"(or reuse the existinghermesscript with ahermes mcp-servesubcommand — smaller surface, avoids a second console_script). - New dependency: add
mcp>=1.28.1(pin loosely; hermes-agent pins exactly 1.28.1 for its own client, but MCP's wire protocol is versioned/negotiated, so exact pinning on the server side is not required — still worth testing against 1.28.1 specifically since that's what's actually installed locally) to hermes-rss'spyproject.tomldependencies (not dev-only — this ships to users). - Registration, one-time per machine:
bash cd /home/matt/hermes-rss uv sync hermes mcp add hermes-rss \ --command uv \ --args run hermes-rss-mcp \ --connect-timeout 10This writes anmcp_servers.hermes-rssblock into~/.hermes/config.yaml(exact key shape UNVERIFIED locally — see MCP section above; confirm viahermes mcp listafter adding, and viacat ~/.hermes/config.yaml— read-only — before assuming field names). - Retire (or keep as fallback) the existing SKILL.md — once the MCP
tools are registered and confirmed working (
hermes mcp test hermes-rss, then a live chat turn),science-recommendationsbecomes redundant for the core list/feedback/explain actions. Recommend keeping the skill only if it documents workflow/judgment ("when NOT to use this," the reasoning_effort gotcha) that doesn't fit into a tool description — MCP tooldescriptionfields are far more space-constrained than a SKILL.md. A slim skill that says "prefer thelist_recommendations/mark_feedback/explain_rankingMCP tools; only fall back to the CLI/curl path inscripts/setup.shif MCP is disconnected" is a reasonable hybrid — do not delete the operational knowledge (thereasoning_effort: medium+ Ollama incompatibility note), just shrink the "how to call it" section.
Effort estimate
- Small, well-scoped task, roughly half a day to a day of focused work for someone already familiar with the hermes-rss codebase:
- ~1-2 hrs: write
mcp_server.pywrapping the 3 core operations withFastMCP, using the official SDK's stdio quick-start pattern (UNVERIFIED exact API surface ofmcp==1.28.1'sFastMCP— the SDK has had breaking changes across 1.x; check the installed version's docs/changelog before writing code, don't assume today'spip show mcpexamples match 1.28.1 exactly). - ~30 min:
pyproject.toml+ entry point wiring,uv sync. - ~30 min:
hermes mcp add+hermes mcp test+ a live smoke-test chat turn exercising each tool. - ~1-2 hrs: buffer for the in-process-vs-proxy-to-existing-server
decision (item 1 above) — if the existing FastAPI route handlers
aren't cleanly importable as plain functions (e.g. they're tightly
coupled to
Request/DependsFastAPI plumbing), the MCP layer may need light refactoring ofsrc/to extract a service-layer function each route and each MCP tool can both call. This is the main variable that could push the estimate to 1-2 days — not assessed yet; would require reading hermes-rss's actual route implementation, which was out of scope for this research pass. - No hermes-agent-side changes needed — MCP client support is already
installed and working (per
pyproject.tomlpin), so this is 100% hermes-rss-side work plus onehermes mcp addcommand. - Compare to the plugin path (not recommended, but for scale): building an
actual
~/.hermes/plugins/hermes-rss/plugin would mean importingtools.registry, matching hermes-agent's exact JSON-Schema tool format, handling theoverride/trust-gate logic if any tool name collides, and re-testing against every hermes-agent minor version bump (plugin API is internal, not a stable public contract) — meaningfully more ongoing maintenance for no additional capability we actually need (we don't need hooks intopre_tool_call/session lifecycle/platform adapters — we need three clean function-calling tools, which MCP already gives us for free with a stable external protocol).
Sources: local read-only inspection of ~/.hermes/hermes-agent
(v0.20.0, confirmed via pyproject.toml) and ~/.hermes/config.yaml;
https://github.com/NousResearch/hermes-agent (README, via WebFetch);
https://github.com/0xNyk/awesome-hermes-agent (via WebFetch);
https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp (via
WebFetch — flagged UNVERIFIED items above were sourced here, not from a
local file read). The mudrii/hermes-agent-docs mirror was not fetched
separately in this pass since the primary docs site and local source
already answered the plugin/MCP questions with higher confidence (direct
code read beats a docs mirror); revisit that source if a
developer/plugin-authoring guide is needed beyond what
hermes_cli/plugins.py's docstrings already provide (which are extensive
and were the primary source for the "Extension points" section above).