Skip to content

feat: add MCP (Model Context Protocol) client support#291

Merged
teknium1 merged 6 commits into
NousResearch:mainfrom
0xbyt4:feature/mcp-client-support
Mar 3, 2026
Merged

feat: add MCP (Model Context Protocol) client support#291
teknium1 merged 6 commits into
NousResearch:mainfrom
0xbyt4:feature/mcp-client-support

Conversation

@0xbyt4

@0xbyt4 0xbyt4 commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add MCP client that connects to external MCP servers via stdio transport, discovers their tools, and registers them into the hermes-agent tool registry
  • Each server runs as a long-lived asyncio Task in a dedicated background event loop, ensuring clean subprocess lifecycle management (no orphan processes)
  • Discovery is idempotent -- safe to call multiple times without creating duplicate connections
  • mcp package is an optional dependency; when not installed, MCP support is silently skipped

Configuration

Add to ~/.hermes/config.yaml:

mcp_servers:
  filesystem:
    command: "npx"
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
  github:
    command: "npx"
    args: ["-y", "@modelcontextprotocol/server-github"]
    env:
      GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_..."

Changes

File Change
tools/mcp_tool.py New -- MCP client module (MCPServerTask, discovery, registration, shutdown)
tests/tools/test_mcp_tool.py New -- 28 unit tests (all mock-based, no real servers)
model_tools.py +7 lines -- call discover_mcp_tools() after built-in tool discovery
cli.py +5 lines -- call shutdown_mcp_servers() in _run_cleanup()
gateway/run.py +5 lines -- call shutdown_mcp_servers() on gateway exit
pyproject.toml +2 lines -- mcp optional dependency group

Architecture

  • Task-per-server: Each MCP server's entire lifecycle (connect, discover, wait, disconnect) runs in a single asyncio Task. This ensures anyio cancel-scope cleanup happens in the same Task that opened the connection, preventing orphan subprocesses.
  • Background event loop: A dedicated asyncio.new_event_loop() runs in a daemon thread. All MCP operations are scheduled via run_coroutine_threadsafe().
  • Sync handlers: Tool handlers are sync (is_async=False) and internally bridge to the background loop, compatible with the registry's dispatch interface.
  • Tool naming: mcp_{server}_{tool} with hyphens/dots sanitized to underscores for LLM API compatibility.
  • Auto-injection: Discovered tools are added to all platform toolsets (hermes-cli, telegram, discord, whatsapp, slack).

Test plan

  • 28 unit tests pass (config loading, schema conversion, check_fn, tool handler, discovery, MCPServerTask lifecycle, toolset injection, graceful fallback, shutdown)
  • Unit tests pass both in isolation and within the full test suite
  • E2E tested with @modelcontextprotocol/server-everything (13 tools): discovery, echo, get_sum, sequential calls, shutdown, orphan process check, re-discovery cycle
  • Idempotency verified: no duplicate connections on repeated discover_mcp_tools() calls
  • No orphan processes after shutdown (verified with pgrep)
  • Graceful degradation when mcp package is not installed

0xbyt4 added 6 commits March 2, 2026 21:03
Connect to external MCP servers via stdio transport, discover their tools
at startup, and register them into the hermes-agent tool registry.

- New tools/mcp_tool.py: config loading, server connection via background
  event loop, tool handler factories, discovery, and graceful shutdown
- model_tools.py: trigger MCP discovery after built-in tool imports
- cli.py: call shutdown_mcp_servers in _run_cleanup
- pyproject.toml: add mcp>=1.2.0 as optional dependency
- 27 unit tests covering config, schema conversion, handlers, registration,
  SDK interaction, toolset injection, graceful fallback, and shutdown

Config format (in ~/.hermes/config.yaml):
  mcp_servers:
    filesystem:
      command: "npx"
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
Ensures MCP subprocess connections are closed when the messaging
gateway shuts down, preventing orphan processes.
Refactor MCP connections from AsyncExitStack to task-per-server
architecture. Each server now runs as a long-lived asyncio Task
with `async with stdio_client(...)`, ensuring anyio cancel-scope
cleanup happens in the same Task that opened the connection.
When discover_mcp_tools() is called multiple times (e.g. direct call
then model_tools import), return existing tool names instead of opening
new connections that would orphan the previous ones.
Patch _servers to empty dict in tests that call discover_mcp_tools()
with mocked config, preventing interference from real MCP connections
that may exist when running within the full test suite.
- Add threading.Lock protecting all shared state (_servers, _mcp_loop, _mcp_thread)
- Fix deadlock in shutdown_mcp_servers: _stop_mcp_loop was called inside
  a _lock block but also acquires _lock (non-reentrant)
- Fix race condition in _ensure_mcp_loop with concurrent callers
- Change idempotency to per-server (retry failed servers, skip connected)
- Dynamic toolset injection via startswith("hermes-") instead of hardcoded list
- Parallel shutdown via asyncio.gather instead of sequential loop
- Add tests for partial failure retry, parallel shutdown, dynamic injection
teknium1 added a commit that referenced this pull request Mar 3, 2026
Upgrades the MCP client implementation from PR #291 with:

- HTTP/Streamable HTTP transport: support 'url' key in config for remote
  MCP servers (Notion, Slack, Sentry, Supabase, etc.)
- Automatic reconnection with exponential backoff (1s-60s, 5 retries)
  when a server connection drops unexpectedly
- Environment variable filtering: only pass safe vars (PATH, HOME, etc.)
  plus user-specified env to stdio subprocesses (prevents secret leaks)
- Credential stripping: sanitize error messages before returning to the
  LLM (strips GitHub PATs, OpenAI keys, Bearer tokens, etc.)
- Configurable per-server timeouts: 'timeout' and 'connect_timeout' keys
- Fix shutdown race condition in servers_snapshot variable scoping

Test coverage: 50 tests (up from 30), including new tests for env
filtering, credential sanitization, HTTP config detection, reconnection
logic, and configurable timeouts.

All 1162 tests pass (1162 passed, 3 skipped, 0 failed).
@teknium1 teknium1 merged commit 468b7fd into NousResearch:main Mar 3, 2026
angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 27, 2026
…ent support

Authored by 0xbyt4. Adds MCP client with official SDK, direct tool registration,
auto-injection into hermes-* toolsets, and graceful degradation.
angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 27, 2026
Upgrades the MCP client implementation from PR NousResearch#291 with:

- HTTP/Streamable HTTP transport: support 'url' key in config for remote
  MCP servers (Notion, Slack, Sentry, Supabase, etc.)
- Automatic reconnection with exponential backoff (1s-60s, 5 retries)
  when a server connection drops unexpectedly
- Environment variable filtering: only pass safe vars (PATH, HOME, etc.)
  plus user-specified env to stdio subprocesses (prevents secret leaks)
- Credential stripping: sanitize error messages before returning to the
  LLM (strips GitHub PATs, OpenAI keys, Bearer tokens, etc.)
- Configurable per-server timeouts: 'timeout' and 'connect_timeout' keys
- Fix shutdown race condition in servers_snapshot variable scoping

Test coverage: 50 tests (up from 30), including new tests for env
filtering, credential sanitization, HTTP config detection, reconnection
logic, and configurable timeouts.

All 1162 tests pass (1162 passed, 3 skipped, 0 failed).
olympus-terminal pushed a commit to olympus-terminal/hermes-agent that referenced this pull request May 16, 2026
…ent support

Authored by 0xbyt4. Adds MCP client with official SDK, direct tool registration,
auto-injection into hermes-* toolsets, and graceful degradation.
olympus-terminal pushed a commit to olympus-terminal/hermes-agent that referenced this pull request May 16, 2026
Upgrades the MCP client implementation from PR NousResearch#291 with:

- HTTP/Streamable HTTP transport: support 'url' key in config for remote
  MCP servers (Notion, Slack, Sentry, Supabase, etc.)
- Automatic reconnection with exponential backoff (1s-60s, 5 retries)
  when a server connection drops unexpectedly
- Environment variable filtering: only pass safe vars (PATH, HOME, etc.)
  plus user-specified env to stdio subprocesses (prevents secret leaks)
- Credential stripping: sanitize error messages before returning to the
  LLM (strips GitHub PATs, OpenAI keys, Bearer tokens, etc.)
- Configurable per-server timeouts: 'timeout' and 'connect_timeout' keys
- Fix shutdown race condition in servers_snapshot variable scoping

Test coverage: 50 tests (up from 30), including new tests for env
filtering, credential sanitization, HTTP config detection, reconnection
logic, and configurable timeouts.

All 1162 tests pass (1162 passed, 3 skipped, 0 failed).
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
…ent support

Authored by 0xbyt4. Adds MCP client with official SDK, direct tool registration,
auto-injection into hermes-* toolsets, and graceful degradation.
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
Upgrades the MCP client implementation from PR NousResearch#291 with:

- HTTP/Streamable HTTP transport: support 'url' key in config for remote
  MCP servers (Notion, Slack, Sentry, Supabase, etc.)
- Automatic reconnection with exponential backoff (1s-60s, 5 retries)
  when a server connection drops unexpectedly
- Environment variable filtering: only pass safe vars (PATH, HOME, etc.)
  plus user-specified env to stdio subprocesses (prevents secret leaks)
- Credential stripping: sanitize error messages before returning to the
  LLM (strips GitHub PATs, OpenAI keys, Bearer tokens, etc.)
- Configurable per-server timeouts: 'timeout' and 'connect_timeout' keys
- Fix shutdown race condition in servers_snapshot variable scoping

Test coverage: 50 tests (up from 30), including new tests for env
filtering, credential sanitization, HTTP config detection, reconnection
logic, and configurable timeouts.

All 1162 tests pass (1162 passed, 3 skipped, 0 failed).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants