--- name: native-mcp description: "MCP client: connect servers, register tools (stdio/HTTP)." version: 1.0.0 author: Hermes Agent license: MIT platforms: [linux, macos, windows] metadata: hermes: tags: [MCP, Tools, Integrations] related_skills: [mcporter] --- # Native MCP Client Hermes Agent has a built-in MCP client that connects to MCP servers at startup, discovers their tools, and makes them available as first-class tools the agent can call directly. No bridge CLI needed -- tools from MCP servers appear alongside built-in tools like `terminal`, `read_file`, etc. ## When to Use Use this whenever you want to: - Connect to MCP servers and use their tools from within Hermes Agent - Add external capabilities (filesystem access, GitHub, databases, APIs) via MCP - Run local stdio-based MCP servers (npx, uvx, or any command) - Connect to remote HTTP/StreamableHTTP MCP servers - Have MCP tools auto-discovered and available in every conversation For ad-hoc, one-off MCP tool calls from the terminal without configuring anything, see the `mcporter` skill instead. ## Prerequisites - **mcp Python package** -- optional dependency; install with `pip install mcp`. If not installed, MCP support is silently disabled. - **Node.js** -- required for `npx`-based MCP servers (most community servers) - **uv** -- required for `uvx`-based MCP servers (Python-based servers) Install the MCP SDK: ```bash pip install mcp # or, if using uv: uv pip install mcp ``` ## Quick Start Add MCP servers to `~/.hermes/config.yaml` under the `mcp_servers` key: ```yaml mcp_servers: time: command: "uvx" args: ["mcp-server-time"] ``` Restart Hermes Agent. On startup it will: 1. Connect to the server 2. Discover available tools 3. Register them with the prefix `mcp_time_*` 4. Inject them into all platform toolsets You can then use the tools naturally -- just ask the agent to get the current time. ## Configuration Reference Each entry under `mcp_servers` is a server name mapped to its config. There are two transport types: **stdio** (command-based) and **HTTP** (url-based). ### Stdio Transport (command + args) ```yaml mcp_servers: server_name: command: "npx" # (required) executable to run args: ["-y", "pkg-name"] # (optional) command arguments, default: [] env: # (optional) environment variables for the subprocess SOME_API_KEY: "value" timeout: 120 # (optional) per-tool-call timeout in seconds, default: 120 connect_timeout: 60 # (optional) initial connection timeout in seconds, default: 60 ``` ### HTTP Transport (url) ```yaml mcp_servers: server_name: url: "https://my-server.example.com/mcp" # (required) server URL headers: # (optional) HTTP headers Authorization: "Bearer sk-..." timeout: 180 # (optional) per-tool-call timeout in seconds, default: 120 connect_timeout: 60 # (optional) initial connection timeout in seconds, default: 60 ``` ### All Config Options | Option | Type | Default | Description | |-------------------|--------|---------|---------------------------------------------------| | `command` | string | -- | Executable to run (stdio transport, required) | | `args` | list | `[]` | Arguments passed to the command | | `env` | dict | `{}` | Extra environment variables for the subprocess | | `url` | string | -- | Server URL (HTTP transport, required) | | `headers` | dict | `{}` | HTTP headers sent with every request | | `timeout` | int | `120` | Per-tool-call timeout in seconds | | `connect_timeout` | int | `60` | Timeout for initial connection and discovery | Note: A server config must have either `command` (stdio) or `url` (HTTP), not both. ## How It Works ### Startup Discovery When Hermes Agent starts, `discover_mcp_tools()` is called during tool initialization: 1. Reads `mcp_servers` from `~/.hermes/config.yaml` 2. For each server, spawns a connection in a dedicated background event loop 3. Initializes the MCP session and calls `list_tools()` to discover available tools 4. Registers each tool in the Hermes tool registry ### Tool Naming Convention MCP tools are registered with the naming pattern: ``` mcp_{server_name}_{tool_name} ``` Hyphens and dots in names are replaced with underscores for LLM API compatibility. Examples: - Server `filesystem`, tool `read_file` → `mcp_filesystem_read_file` - Server `github`, tool `list-issues` → `mcp_github_list_issues` - Server `my-api`, tool `fetch.data` → `mcp_my_api_fetch_data` ### Auto-Injection After discovery, MCP tools are automatically injected into all `hermes-*` platform toolsets (CLI, Discord, Telegram, etc.). This means MCP tools are available in every conversation without any additional configuration. ### Connection Lifecycle - Each server runs as a long-lived asyncio Task in a background daemon thread - Connections persist for the lifetime of the agent process - If a connection drops, automatic reconnection with exponential backoff kicks in (up to 5 retries, max 60s backoff) - On agent shutdown, all connections are gracefully closed ### Idempotency `discover_mcp_tools()` is idempotent -- calling it multiple times only connects to servers that aren't already connected. Failed servers are retried on subsequent calls. ## Transport Types ### Stdio Transport The most common transport. Hermes launches the MCP server as a subprocess and communicates over stdin/stdout. ```yaml mcp_servers: filesystem: command: "npx" args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"] ``` The subprocess inherits a **filtered** environment (see Security section below) plus any variables you specify in `env`. ### HTTP / StreamableHTTP Transport For remote or shared MCP servers. Requires the `mcp` package to include HTTP client support (`mcp.client.streamable_http`). ```yaml mcp_servers: remote_api: url: "https://mcp.example.com/mcp" headers: Authorization: "Bearer sk-..." ``` If HTTP support is not available in your installed `mcp` version, the server will fail with an ImportError and other servers will continue normally. ## Security ### Environment Variable Filtering For stdio servers, Hermes does NOT pass your full shell environment to MCP subprocesses. Only safe baseline variables are inherited: - `PATH`, `HOME`, `USER`, `LANG`, `LC_ALL`, `TERM`, `SHELL`, `TMPDIR` - Any `XDG_*` variables All other environment variables (API keys, tokens, secrets) are excluded unless you explicitly add them via the `env` config key. This prevents accidental credential leakage to untrusted MCP servers. ```yaml mcp_servers: github: command: "npx" args: ["-y", "@modelcontextprotocol/server-github"] env: # Only this token is passed to the subprocess GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_..." ``` ### Credential Stripping in Error Messages If an MCP tool call fails, any credential-like patterns in the error message are automatically redacted before being shown to the LLM. This covers: - GitHub PATs (`ghp_...`) - OpenAI-style keys (`sk-...`) - Bearer tokens - Generic `token=`, `key=`, `API_KEY=`, `password=`, `secret=` patterns ## Troubleshooting ### "MCP SDK not available -- skipping MCP tool discovery" The `mcp` Python package is not installed. Install it: ```bash pip install mcp ``` ### "No MCP servers configured" No `mcp_servers` key in `~/.hermes/config.yaml`, or it's empty. Add at least one server. ### "Failed to connect to MCP server 'X'" Common causes: - **Command not found**: The `command` binary isn't on PATH. Ensure `npx`, `uvx`, or the relevant command is installed. - **Package not found**: For npx servers, the npm package may not exist or may need `-y` in args to auto-install. - **Timeout**: The server took too long to start. Increase `connect_timeout`. - **Port conflict**: For HTTP servers, the URL may be unreachable. ### "MCP server 'X' requires HTTP transport but mcp.client.streamable_http is not available" Your `mcp` package version doesn't include HTTP client support. Upgrade: ```bash pip install --upgrade mcp ``` ### Tools not appearing - Check that the server is listed under `mcp_servers` (not `mcp` or `servers`) - Ensure the YAML indentation is correct - Look at Hermes Agent startup logs for connection messages - Tool names are prefixed with `mcp_{server}_{tool}` -- look for that pattern ### MCP server shows "SEARXNG_URL is not set" even though web.searxng_url is configured `web.searxng_url` in `config.yaml` only affects Hermes's built-in `web_search`/`web_extract` tools. The `mcp-searxng` MCP server subprocess reads `SEARXNG_URL` from its **environment**, not from config.yaml. These are two separate code paths. Fix: add `SEARXNG_URL` explicitly to the server's `env` block in config.yaml AND to `~/.hermes/profiles//.env`: ```yaml mcp_servers: searxng: command: npx args: ["-y", "mcp-searxng"] env: SEARXNG_URL: "http://10.0.0.8:8888" # required — not inherited from web.searxng_url ``` ```bash # Also add to .env so it's available to the shell process echo 'SEARXNG_URL=http://10.0.0.8:8888' >> ~/.hermes/profiles//.env ``` General rule: **any MCP stdio server subprocess only sees env vars explicitly passed via `env:` in its config block**. Hermes strips the shell env for security (see Environment Variable Filtering above). ### Connection keeps dropping The client retries up to 5 times with exponential backoff (1s, 2s, 4s, 8s, 16s, capped at 60s). If the server is fundamentally unreachable, it gives up after 5 attempts. Check the server process and network connectivity. ## Cross-Network Bridge via SSH-Stdio When the MCP server you want to use lives on a different host (different LXC, VM, container, or jump box), and the server only supports stdio transport, bridge it through SSH. This is the simplest cross-host MCP pattern — no HTTP server, no reverse proxy, no firewall holes beyond SSH. **Pattern:** point the MCP client's `command` at `ssh`, with the remote shell command being the stdio MCP server invocation. ```yaml mcp_servers: remote-hermes: command: "ssh" args: - "-o" - "StrictHostKeyChecking=accept-new" - "-o" - "ServerAliveInterval=30" - "user@remote-host" - "cd /path/to/server && exec python3 -m some.mcp_server" timeout: 300 ``` In Claude Code's `~/.claude.json`: ```json { "mcpServers": { "remote-hermes": { "command": "ssh", "args": [ "-o", "StrictHostKeyChecking=accept-new", "-o", "ServerAliveInterval=30", "user@remote-host", "cd /path/to/server && exec python3 -m some.mcp_server" ], "env": {} } } } ``` **Key conventions:** - `exec` the remote command so Ctrl-C propagates from the SSH process to the server process (no extra shell layer). - `ServerAliveInterval=30` prevents idle disconnects on long-running sessions. - `StrictHostKeyChecking=accept-new` accepts the host key on first connect without prompting. Use `yes` instead if you want one-shot trust. **When to use SSH-stdio vs HTTP transport:** - Server is stdio-only (e.g., FastMCP apps that don't expose HTTP) → **SSH-stdio** - Server is on the same host → either, prefer stdio - Server is on a different network and supports HTTP → **HTTP** (cleaner, supports multiple concurrent clients) - Server is on a different network and is stdio-only → **SSH-stdio** is the only zero-infrastructure option **Pitfall: stateless tool callbacks can't drive agent-loop tools.** When wrapping a tool server in MCP, the MCP callback is stateless — it gets a request, calls a function, returns a response. If the tool needs the running agent's mid-loop state (open files, conversation context, subagent handles, in-memory cache), the MCP callback cannot reach it. Typical examples: `terminal` (needs persistent shell handle), `delegate_task` (needs the agent loop to dispatch), `memory` (needs the memory provider instance), `session_search` (needs the session DB connection). When designing an MCP-wrappable tool, it must be a pure function from arguments to result, or close to it. ## Examples ### Time Server (uvx) ```yaml mcp_servers: time: command: "uvx" args: ["mcp-server-time"] ``` Registers tools like `mcp_time_get_current_time`. ### Filesystem Server (npx) ```yaml mcp_servers: filesystem: command: "npx" args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/documents"] timeout: 30 ``` Registers tools like `mcp_filesystem_read_file`, `mcp_filesystem_write_file`, `mcp_filesystem_list_directory`. ### GitHub Server with Authentication ```yaml mcp_servers: github: command: "npx" args: ["-y", "@modelcontextprotocol/server-github"] env: GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_xxxxxxxxxxxxxxxxxxxx" timeout: 60 ``` Registers tools like `mcp_github_list_issues`, `mcp_github_create_pull_request`, etc. ### Remote HTTP Server ### Remote HTTP Server ```yaml mcp_servers: company_api: url: "https://mcp.mycompany.com/v1/mcp" headers: Authorization: "Bearer sk-xxxxxxxxxxxxxxxxxxxx" X-Team-Id: "engineering" timeout: 180 connect_timeout: 30 ``` ### SearXNG (local instance, stdio via npx) ```yaml mcp_servers: searxng: command: npx args: ["-y", "mcp-searxng"] env: SEARXNG_URL: "http://10.0.0.8:8888" timeout: 60 connect_timeout: 30 ``` Registers `mcp_searxng_searxng_web_search`, `mcp_searxng_web_url_read`, `mcp_searxng_searxng_search_suggestions`, `mcp_searxng_searxng_instance_info`. Package: `mcp-searxng` (npm, v1.5.0). Use alongside `web.backend: searxng` in config.yaml — the MCP tools give structured tool-call access, while `web.backend` drives the built-in `web_search`/`web_extract` tools. ### Multiple Servers ```yaml mcp_servers: time: command: "uvx" args: ["mcp-server-time"] filesystem: command: "npx" args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] github: command: "npx" args: ["-y", "@modelcontextprotocol/server-github"] env: GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_xxxxxxxxxxxxxxxxxxxx" company_api: url: "https://mcp.internal.company.com/mcp" headers: Authorization: "Bearer sk-xxxxxxxxxxxxxxxxxxxx" timeout: 300 ``` All tools from all servers are registered and available simultaneously. Each server's tools are prefixed with its name to avoid collisions. ## Sampling (Server-Initiated LLM Requests) Hermes supports MCP's `sampling/createMessage` capability — MCP servers can request LLM completions through the agent during tool execution. This enables agent-in-the-loop workflows (data analysis, content generation, decision-making). Sampling is **enabled by default**. Configure per server: ```yaml mcp_servers: my_server: command: "npx" args: ["-y", "my-mcp-server"] sampling: enabled: true # default: true model: "gemini-3-flash" # model override (optional) max_tokens_cap: 4096 # max tokens per request timeout: 30 # LLM call timeout (seconds) max_rpm: 10 # max requests per minute allowed_models: [] # model whitelist (empty = all) max_tool_rounds: 5 # tool loop limit (0 = disable) log_level: "info" # audit verbosity ``` Servers can also include `tools` in sampling requests for multi-turn tool-augmented workflows. The `max_tool_rounds` config prevents infinite tool loops. Per-server audit metrics (requests, errors, tokens, tool use count) are tracked via `get_mcp_status()`. Disable sampling for untrusted servers with `sampling: { enabled: false }`. ## Notes - MCP tools are called synchronously from the agent's perspective but run asynchronously on a dedicated background event loop - Tool results are returned as JSON with either `{"result": "..."}` or `{"error": "..."}` - The native MCP client is independent of `mcporter` -- you can use both simultaneously - Server connections are persistent and shared across all conversations in the same agent process - Adding or removing servers requires restarting the agent (no hot-reload currently)