How Hermes manages its capability surface across three layers
Hermes has three distinct systems for extending what the agent can do. They form a layered stack:
Each layer serves a different purpose. Built-in tools are actions the agent can take. MCP tools are external actions the agent discovers at startup. Skills are knowledge the agent reads on-demand to do its job better. They stack: a skill can teach the agent to use MCP tools effectively, and MCP tools work alongside built-in tools in the same conversation.
Every built-in tool lives in a single file under tools/ and self-registers at import time into the ToolRegistry singleton. This is the central pattern you must understand:
Each tool provides four things when it registers:
registry.register(
name="terminal", # unique tool name the LLM calls
toolset="terminal", # which toolset it belongs to
schema={...}, # JSON schema the LLM sees as a function
handler=lambda args: ..., # actual implementation — returns JSON string
check_fn=requirements, # optional: returns True/False for availability
requires_env=["API_KEY"], # optional: env vars needed
)
Auto-discovery: Any tools/*.py with a top-level registry.register() call is auto-imported. No manual list to maintain.
Conditional gating: check_fn lets tools appear only when requirements are met (e.g. websockets package installed, CDP endpoint configured, API key present).
Toolsets: hermes tools enable/disable <toolset> controls entire groups. Toolsets are per-platform (CLI can have different tools than Discord).
No core changes: Adding a tool is one file. Zero changes to the agent core or model_tools.py.
Anthropic recommends sending ≤20 tools per API call for optimal Claude performance. Hermes often ships more (~28 with browser), which is a real tradeoff: more tools means the model has more options but can also struggle with selection quality. Hermes addresses this with toolsets — you can disable unnecessary toolsets per-session or per-platform. If you're building a similar system, design your tool schema filtering before you have 60+ tools. See Lesson 01: "The Core Is Narrow" is Rule #3.
The check_fn is what makes "system dependency not met" warnings appear in hermes tools list. It's a simple function:
# Some tools are only available on certain platforms
def check_requirements() -> bool:
return bool(os.getenv("MY_API_KEY"))
# Some need external packages installed
def check():
try:
import websockets
return True
except ImportError:
return False
# If check_fn returns False, the tool doesn't appear in the LLM's schema list
# and the model can't call it. No error — it's just invisible.
MCP (Model Context Protocol) connects Hermes to external tool servers — GitHub, filesystem, databases, Supabase, browser automation — as if they were built-in tools. They're discovered at startup and registered into the same ToolRegistry with a mcp_<server>_<tool> prefix.
Three connection types:
# 1. Stdio — local subprocess (most common)
# Runs as a child process, communicates over stdin/stdout
mcp_servers:
github:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_TOKEN: "ghp_..."
# 2. HTTP — remote endpoint
# Direct connection to a remote MCP server
mcp_servers:
my_api:
url: "https://mcp.example.com/mcp"
headers:
Authorization: "Bearer sk-..."
# 3. HTTP + OAuth — for services like Google Drive, Linear
mcp_servers:
google_slides:
url: "https://mcp.example.com/mcp"
auth: oauth
Once registered, MCP tools look identical to built-in tools from the LLM's perspective. The only difference is the name prefix. For example, with a server called github:
| MCP server | Tool name in server | Hermes tool name |
|---|---|---|
| github | list_issues | mcp_github_list_issues |
| filesystem | read_file | mcp_filesystem_read_file |
| time | get_current_time | mcp_time_get_current_time |
| supabase | execute_sql | mcp_supabase_execute_sql |
The agent sees all of these alongside terminal, read_file, web_search, etc. It doesn't know which are built-in vs MCP — it just sees a flat list of callable functions.
Hermes does NOT pass your full shell environment to MCP subprocesses. Only PATH, HOME, USER, LANG, and a few others are inherited. All API keys must be explicitly added via the env: key in the server config. This prevents credential leakage to untrusted MCP servers.
Both tools and skills use progressive disclosure, but in different ways:
When the Tool Search bridge is active (default: auto, config at tools.tool_search in config.yaml), MCP and plugin tools are replaced in the model-visible schema by three bridge tools:
# These 3 tools replace ALL deferrable MCP/plugin tools in the API call
tool_search(query, limit) → Finds deferrable tools by name/keyword, returns catalog
tool_describe(name) → Returns the full JSON schema for one deferrable tool
tool_call(name, arguments) → Invokes a deferrable tool with the given arguments
Default mode: auto — if all deferrable tools would consume ≥10% of the context window, they're replaced by the 3 bridge tools. If less than 10%, they pass through unchanged. This means on low-tool-count sessions, you see all tools; on MCP-heavy sessions, the bridge kicks in automatically.
Core tools are NEVER deferred. The 18-28 built-in tools (terminal, file, web, browser, etc.) always ship in the schema. Only MCP and plugin-registered tools can be deferred.
| What | Progressive disclosure? | How |
|---|---|---|
| Core built-in tools | ❌ Never deferred | Always in schema (18-28 per session) |
| MCP + plugin tools | ✅ Via Tool Search bridge | Replaced by 3 bridge tools when threshold hit (≥10% context) |
| Skills | ✅ Always | Only names in prompt; full content loaded via skill_view() |
Without progressive disclosure, every MCP server you add ships its tool schemas on every API call. With just 3-4 MCP servers, you could easily hit 40-50 total tools. The Tool Search bridge keeps the active schema lean by deferring MCP/plugin tools until the agent asks for them by name. If you're building a similar system, design your tool schema filtering before you have 60+ tools — adding progressive disclosure after is much harder.
Skills are knowledge documents, not executable tools. They always use progressive disclosure — only names + descriptions go in the system prompt, and the agent must explicitly call skill_view() to load full content:
| Level | Tool | What's loaded | Token cost |
|---|---|---|---|
| 0 | skills_list() | Skill names + descriptions only | ~3K tokens |
| 1 | skill_view(name) | Full SKILL.md content (frontmatter + body) | Varies |
| 2 | skill_view(name, file_path) | Specific reference file (scripts, templates) | Depends on file |
The system prompt only contains the index (Level 0). The agent must explicitly call skill_view() to get details. This is deliberate — 60+ skills at full content would blow the context window.
# SKILL.md with YAML frontmatter
---
name: my-skill
description: "Brief description shown in skills_list()"
version: 1.0.0
platforms: [linux, macos]
metadata:
hermes:
tags: [python, devops]
requires_toolsets: [terminal] # Only shown if terminal is enabled
fallback_for_toolsets: [web] # Shown when web tools are unavailable
config:
- key: my.setting
description: "Control setting"
default: "value"
---
## Procedure
1. Step one with exact commands
2. Step two with pitfalls noted
3. Verification step
This is the self-improvement loop in action. The agent can autonomously create and maintain skills using skill_manage:
# After solving a complex multi-step problem, the agent saves the approach
skill_manage(
action="create",
name="deploy-fastapi",
content="---\nname: deploy-fastapi\n...\n---\n## Steps\n..."
)
# To fix a stale skill mid-use
skill_manage(
action="patch",
name="deploy-fastapi",
old_string="old command",
new_string="new command",
)
The agent can have all three in the same conversation. A skill might contain a step like "use the mcp_supabase_execute_sql tool with this query", teaching the agent how to combine MCP tools with its built-in knowledge.
| Command | What it does |
|---|---|
hermes tools list | Show all toolsets with ✅/❌ availability |
hermes tools enable <name> | Enable a toolset for future sessions |
hermes mcp catalog | Browse curated MCP servers |
hermes mcp install <name> | Install an MCP server from catalog |
hermes mcp list | List configured MCP servers |
hermes mcp test <name> | Test connectivity to a server |
hermes mcp configure <name> | Toggle specific tool enable/disable |
hermes skills list | List all installed skills |
hermes skills search <q> | Search skills hub |
hermes skills install <id> | Install a skill from hub |
hermes skills browse | Browse all available skills |
/reload-mcp | Slash command to reload MCP servers mid-session |
registry.register() — one file, zero core changesmcp_<server>_<tool> prefix/reload-mcp slash command also triggers a refresh mid-session without a full restart. The key: it's NOT automatic — you must restart or run /reload-mcp after adding a server.