Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,23 @@ hermes tools

**Available toolsets:** `web`, `terminal`, `file`, `browser`, `vision`, `image_gen`, `moa`, `skills`, `tts`, `todo`, `memory`, `session_search`, `cronjob`, `code_execution`, `delegation`, `clarify`, and more.

### 🔌 MCP (Model Context Protocol)

Connect to any MCP-compatible server to extend Hermes with external tools. Just add servers to your config:

```yaml
mcp_servers:
time:
command: uvx
args: ["mcp-server-time"]
notion:
url: https://mcp.notion.com/mcp
```

Supports stdio and HTTP transports, auto-reconnection, and env var filtering. See [docs/mcp.md](docs/mcp.md) for details.

Install MCP support: `pip install hermes-agent[mcp]`

### 🖥️ Terminal & Process Management

The terminal tool can execute commands in different environments, with full background process management via the `process` tool:
Expand Down
38 changes: 16 additions & 22 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,33 +63,27 @@ Full Python plugin interface that goes beyond the current hook system.
- `hermes plugin list|install|uninstall|create` CLI commands
- Plugin discovery and validation on startup

### Phase 3: MCP support (industry standard)
- MCP client that can connect to external MCP servers (stdio, SSE, HTTP)
- This is the big one -- Codex, Cline, and OpenCode all support MCP
- Allows Hermes to use any MCP-compatible tool server (hundreds exist)
- Config: `mcp_servers` list in config.yaml with connection details
- Each MCP server's tools get registered as a new toolset
### Phase 3: MCP support (industry standard) ✅ DONE
- ✅ MCP client that connects to external MCP servers (stdio + HTTP/StreamableHTTP)
- ✅ Config: `mcp_servers` in config.yaml with connection details
- ✅ Each MCP server's tools auto-registered as a dynamic toolset
- Future: Resources, Prompts, Progress notifications, `hermes mcp` CLI command

---

## 6. MCP (Model Context Protocol) Support 🔗
## 6. MCP (Model Context Protocol) Support 🔗 ✅ DONE

**Status:** Not started
**Priority:** High -- this is becoming an industry standard

MCP is the protocol that Codex, Cline, and OpenCode all support for connecting to external tool servers. Supporting MCP would instantly give Hermes access to hundreds of community tool servers.
**Status:** Implemented (PR #301)
**Priority:** Complete

**What other agents do:**
- **Codex**: Full MCP integration with skill dependencies
- **Cline**: `use_mcp_tool` / `access_mcp_resource` / `load_mcp_documentation` tools
- **OpenCode**: MCP client support (stdio, SSE, StreamableHTTP transports), OAuth auth
Native MCP client support with stdio and HTTP/StreamableHTTP transports, auto-discovery, reconnection with exponential backoff, env var filtering, and credential stripping. See `docs/mcp.md` for full documentation.

**Our approach:**
- Implement an MCP client that can connect to external MCP servers
- Config: list of MCP servers in `~/.hermes/config.yaml` with transport type and connection details
- Each MCP server's tools auto-registered as a dynamic toolset
- Start with stdio transport (most common), then add SSE and HTTP
- Could also be part of the Plugin system (#5, Phase 3) since MCP is essentially a plugin protocol
**Still TODO:**
- `hermes mcp` CLI subcommand (list/test/status)
- `hermes tools` UI integration for MCP toolsets
- MCP Resources and Prompts support
- OAuth authentication for remote servers
- Progress notifications for long-running tools

---

Expand Down Expand Up @@ -121,7 +115,7 @@ Automatic filesystem snapshots after each agent loop iteration so the user can r

### Tier 1: Next Up

1. MCP Support -- #6
1. ~~MCP Support -- #6~~ ✅ Done (PR #301)

### Tier 2: Quality of Life

Expand Down
35 changes: 35 additions & 0 deletions cli-config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,41 @@ toolsets:
# toolsets:
# - safe

# =============================================================================
# MCP (Model Context Protocol) Servers
# =============================================================================
# Connect to external MCP servers to add tools from the MCP ecosystem.
# Each server's tools are automatically discovered and registered.
# See docs/mcp.md for full documentation.
#
# Stdio servers (spawn a subprocess):
# command: the executable to run
# args: command-line arguments
# env: environment variables (only these + safe defaults passed to subprocess)
#
# HTTP servers (connect to a URL):
# url: the MCP server endpoint
# headers: HTTP headers (e.g., for authentication)
#
# Optional per-server settings:
# timeout: tool call timeout in seconds (default: 120)
# connect_timeout: initial connection timeout (default: 60)
#
# mcp_servers:
# time:
# command: uvx
# args: ["mcp-server-time"]
# filesystem:
# command: npx
# args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"]
# notion:
# url: https://mcp.notion.com/mcp
# github:
# command: npx
# args: ["-y", "@modelcontextprotocol/server-github"]
# env:
# GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_..."

# =============================================================================
# Voice Transcription (Speech-to-Text)
# =============================================================================
Expand Down
93 changes: 93 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,11 @@ def _run_cleanup():
_cleanup_all_browsers()
except Exception:
pass
try:
from tools.mcp_tool import shutdown_mcp_servers
shutdown_mcp_servers()
except Exception:
pass

# ============================================================================
# ASCII Art & Branding
Expand Down Expand Up @@ -685,6 +690,7 @@ def build_welcome_banner(console: Console, model: str, cwd: str, tools: List[dic
"/cron": "Manage scheduled tasks (list, add, remove)",
"/skills": "Search, install, inspect, or manage skills from online registries",
"/platforms": "Show gateway/messaging platform status",
"/reload-mcp": "Reload MCP servers from config.yaml",
"/quit": "Exit the CLI (also: /exit, /q)",
}

Expand Down Expand Up @@ -1765,6 +1771,8 @@ def process_command(self, command: str) -> bool:
self._manual_compress()
elif cmd_lower == "/usage":
self._show_usage()
elif cmd_lower == "/reload-mcp":
self._reload_mcp()
else:
# Check for skill slash commands (/gif-search, /axolotl, etc.)
base_cmd = cmd_lower.split()[0]
Expand Down Expand Up @@ -1886,6 +1894,91 @@ def _show_usage(self):
for quiet_logger in ('tools', 'minisweagent', 'run_agent', 'trajectory_compressor', 'cron', 'hermes_cli'):
logging.getLogger(quiet_logger).setLevel(logging.ERROR)

def _reload_mcp(self):
"""Reload MCP servers: disconnect all, re-read config.yaml, reconnect.

After reconnecting, refreshes the agent's tool list so the model
sees the updated tools on the next turn.
"""
try:
from tools.mcp_tool import shutdown_mcp_servers, discover_mcp_tools, _load_mcp_config, _servers, _lock

# Capture old server names
with _lock:
old_servers = set(_servers.keys())

print("🔄 Reloading MCP servers...")

# Shutdown existing connections
shutdown_mcp_servers()

# Reconnect (reads config.yaml fresh)
new_tools = discover_mcp_tools()

# Compute what changed
with _lock:
connected_servers = set(_servers.keys())

added = connected_servers - old_servers
removed = old_servers - connected_servers
reconnected = connected_servers & old_servers

if reconnected:
print(f" ♻️ Reconnected: {', '.join(sorted(reconnected))}")
if added:
print(f" ➕ Added: {', '.join(sorted(added))}")
if removed:
print(f" ➖ Removed: {', '.join(sorted(removed))}")
if not connected_servers:
print(" No MCP servers connected.")
else:
print(f" 🔧 {len(new_tools)} tool(s) available from {len(connected_servers)} server(s)")

# Refresh the agent's tool list so the model can call new tools
if self.agent is not None:
from model_tools import get_tool_definitions
self.agent.tools = get_tool_definitions(
enabled_toolsets=self.agent.enabled_toolsets
if hasattr(self.agent, "enabled_toolsets") else None,
quiet_mode=True,
)
self.agent.valid_tool_names = {
tool["function"]["name"] for tool in self.agent.tools
} if self.agent.tools else set()

# Inject a message at the END of conversation history so the
# model knows tools changed. Appended after all existing
# messages to preserve prompt-cache for the prefix.
change_parts = []
if added:
change_parts.append(f"Added servers: {', '.join(sorted(added))}")
if removed:
change_parts.append(f"Removed servers: {', '.join(sorted(removed))}")
if reconnected:
change_parts.append(f"Reconnected servers: {', '.join(sorted(reconnected))}")
tool_summary = f"{len(new_tools)} MCP tool(s) now available" if new_tools else "No MCP tools available"
change_detail = ". ".join(change_parts) + ". " if change_parts else ""
self.conversation_history.append({
"role": "user",
"content": f"[SYSTEM: MCP servers have been reloaded. {change_detail}{tool_summary}. The tool list for this conversation has been updated accordingly.]",
})

# Persist session immediately so the session log reflects the
# updated tools list (self.agent.tools was refreshed above).
if self.agent is not None:
try:
self.agent._persist_session(
self.conversation_history,
self.conversation_history,
)
except Exception:
pass # Best-effort

print(f" ✅ Agent updated — {len(self.agent.tools if self.agent else [])} tool(s) available")

except Exception as e:
print(f" ❌ MCP reload failed: {e}")

def _clarify_callback(self, question, choices):
"""
Platform callback for the clarify tool. Called from the agent thread.
Expand Down
Loading