From 60938d5bab0bff66306e3ef7232a624e8380529d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:41:13 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20hermes=20mcp=20serve=20--tools=20?= =?UTF-8?q?=E2=80=94=20expose=20Hermes'=20tool=20surface=20to=20any=20MCP?= =?UTF-8?q?=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promotes the curated hermes-tools MCP surface (previously reachable only via the codex_app_server runtime) to a first-class flag on 'hermes mcp serve'. With --tools, external MCP clients (Claude Code, Codex, Cursor, custom harnesses) get Hermes' web search/extract, browser automation, vision, image generation, skills, and TTS tools alongside the existing messaging bridge, under the credentials configured in the local Hermes install. - agent/transports/hermes_tools_mcp_server.py: factor tool registration into register_hermes_tools(mcp) so both entry points share one surface - mcp_serve.py: create_mcp_server(include_tools=) + run_mcp_server flag - hermes_cli/subcommands/mcp.py: --tools flag on 'mcp serve' - hermes_cli/mcp_config.py: thread the flag through the dispatcher - docs: mcp.md section with client config example --- agent/transports/hermes_tools_mcp_server.py | 58 +++++++++++++-------- hermes_cli/mcp_config.py | 5 +- hermes_cli/subcommands/mcp.py | 9 ++++ mcp_serve.py | 39 ++++++++++++-- tests/test_mcp_serve.py | 13 ++++- website/docs/user-guide/features/mcp.md | 30 ++++++++++- 6 files changed, 126 insertions(+), 28 deletions(-) diff --git a/agent/transports/hermes_tools_mcp_server.py b/agent/transports/hermes_tools_mcp_server.py index 7fc23cf506d96..27aff089e8753 100644 --- a/agent/transports/hermes_tools_mcp_server.py +++ b/agent/transports/hermes_tools_mcp_server.py @@ -149,34 +149,25 @@ EXPOSED_TOOLS: tuple[str, ...] = ( ) -def _build_server() -> Any: - """Create the FastMCP server with Hermes tools attached. Lazy imports - so the module can be imported without the mcp package installed - (we degrade to a clear error only when actually run).""" - try: - from mcp.server.fastmcp import FastMCP - except ImportError as exc: # pragma: no cover - install hint - raise ImportError( - f"hermes-tools MCP server requires the 'mcp' package: {exc}" - ) from exc +def register_hermes_tools(mcp: Any) -> int: + """Register the curated Hermes tool surface onto an existing FastMCP server. + Shared by two entry points: the codex_app_server runtime's dedicated + ``hermes-tools`` stdio server (``_build_server`` below) and + ``hermes mcp serve --tools`` (``mcp_serve.py``), which folds these tools + into the conversation-bridge server so any MCP client gets Hermes' + web/browser/vision/image/skills/TTS capabilities alongside messaging. + + Returns the number of tools actually registered (tools missing from the + current process's registry — unconfigured backends, disabled toolsets — + are skipped, matching the model-facing availability rules). + """ # Discover Hermes tools so dispatch works. from model_tools import ( get_tool_definitions, handle_function_call, ) - mcp = FastMCP( - "hermes-tools", - instructions=( - "Hermes Agent's tool surface, exposed for use inside a Codex " - "session. Use these for capabilities Codex's built-in toolset " - "doesn't cover: web search/extract, browser automation, " - "subagent delegation, vision, image generation, persistent " - "memory, skills, and cross-session search." - ), - ) - # Pull authoritative Hermes tool schemas for the ones we expose, so # MCP clients see the same parameter docs Hermes gives the model. all_defs = { @@ -242,6 +233,31 @@ def _build_server() -> Any: exposed_count, len(EXPOSED_TOOLS), ) + return exposed_count + + +def _build_server() -> Any: + """Create the FastMCP server with Hermes tools attached. Lazy imports + so the module can be imported without the mcp package installed + (we degrade to a clear error only when actually run).""" + try: + from mcp.server.fastmcp import FastMCP + except ImportError as exc: # pragma: no cover - install hint + raise ImportError( + f"hermes-tools MCP server requires the 'mcp' package: {exc}" + ) from exc + + mcp = FastMCP( + "hermes-tools", + instructions=( + "Hermes Agent's tool surface, exposed for use inside a Codex " + "session. Use these for capabilities Codex's built-in toolset " + "doesn't cover: web search/extract, browser automation, " + "subagent delegation, vision, image generation, persistent " + "memory, skills, and cross-session search." + ), + ) + register_hermes_tools(mcp) return mcp diff --git a/hermes_cli/mcp_config.py b/hermes_cli/mcp_config.py index 0f5f55ba35962..d1b7ea1acb915 100644 --- a/hermes_cli/mcp_config.py +++ b/hermes_cli/mcp_config.py @@ -1076,7 +1076,10 @@ def mcp_command(args): if action == "serve": from mcp_serve import run_mcp_server - run_mcp_server(verbose=getattr(args, "verbose", False)) + run_mcp_server( + verbose=getattr(args, "verbose", False), + include_tools=getattr(args, "tools", False), + ) return # Catalog subcommands live in mcp_picker / mcp_catalog. Import lazily so diff --git a/hermes_cli/subcommands/mcp.py b/hermes_cli/subcommands/mcp.py index 387a88e85ad5e..95825fec18731 100644 --- a/hermes_cli/subcommands/mcp.py +++ b/hermes_cli/subcommands/mcp.py @@ -36,6 +36,15 @@ def build_mcp_parser(subparsers, *, cmd_mcp: Callable) -> None: action="store_true", help="Enable verbose logging on stderr", ) + mcp_serve_p.add_argument( + "--tools", + action="store_true", + help=( + "Also expose Hermes' tool surface (web search/extract, browser " + "automation, vision, image generation, skills, TTS) so external " + "MCP clients can use Hermes' connectors directly" + ), + ) add_accept_hooks_flag(mcp_serve_p) mcp_add_p = mcp_sub.add_parser( diff --git a/mcp_serve.py b/mcp_serve.py index 23f248164b45d..55746939d44b7 100644 --- a/mcp_serve.py +++ b/mcp_serve.py @@ -587,8 +587,18 @@ class EventBridge: # MCP Server # --------------------------------------------------------------------------- -def create_mcp_server(event_bridge: Optional[EventBridge] = None) -> "FastMCP": - """Create and return the Hermes MCP server with all tools registered.""" +def create_mcp_server( + event_bridge: Optional[EventBridge] = None, + include_tools: bool = False, +) -> "FastMCP": + """Create and return the Hermes MCP server with all tools registered. + + When ``include_tools`` is True, the curated Hermes tool surface + (web search/extract, browser automation, vision, image generation, + skills, TTS, kanban) is registered alongside the messaging bridge, so + external MCP clients (Claude Code, Codex, Cursor, ...) can use Hermes' + connectors and capabilities directly. + """ if not _MCP_SERVER_AVAILABLE: raise ImportError( "MCP server requires the 'mcp' package. " @@ -601,9 +611,25 @@ def create_mcp_server(event_bridge: Optional[EventBridge] = None) -> "FastMCP": "Hermes Agent messaging bridge. Use these tools to interact with " "conversations across Telegram, Discord, Slack, WhatsApp, Signal, " "Matrix, and other connected platforms." + + ( + " Also exposes Hermes' own capabilities — web search/extract, " + "browser automation, vision analysis, image generation, " + "skills, and text-to-speech — as directly callable tools." + if include_tools + else "" + ) ), ) + if include_tools: + # Fold in Hermes' curated tool surface. Registered first so the + # count reflects what this process can actually dispatch + # (unconfigured backends are skipped by availability checks). + from agent.transports.hermes_tools_mcp_server import register_hermes_tools + + registered = register_hermes_tools(mcp) + logger.info("exposed %d Hermes tools over MCP", registered) + bridge = event_bridge or EventBridge() # -- conversations_list ------------------------------------------------ @@ -1003,7 +1029,7 @@ def create_mcp_server(event_bridge: Optional[EventBridge] = None) -> "FastMCP": # Entry point # --------------------------------------------------------------------------- -def run_mcp_server(verbose: bool = False) -> None: +def run_mcp_server(verbose: bool = False, include_tools: bool = False) -> None: """Start the Hermes MCP server on stdio.""" if not _MCP_SERVER_AVAILABLE: print( @@ -1018,10 +1044,15 @@ def run_mcp_server(verbose: bool = False) -> None: else: logging.basicConfig(level=logging.WARNING, stream=sys.stderr) + if include_tools: + # Keep Hermes' own banners/prints off stdout — it is the MCP wire. + os.environ.setdefault("HERMES_QUIET", "1") + os.environ.setdefault("HERMES_REDACT_SECRETS", "true") + bridge = EventBridge() bridge.start() - server = create_mcp_server(event_bridge=bridge) + server = create_mcp_server(event_bridge=bridge, include_tools=include_tools) import asyncio diff --git a/tests/test_mcp_serve.py b/tests/test_mcp_serve.py index 6c587965779cf..9764d8a7d4494 100644 --- a/tests/test_mcp_serve.py +++ b/tests/test_mcp_serve.py @@ -1012,7 +1012,18 @@ class TestCliIntegration: args = argparse.Namespace(mcp_action="serve", verbose=True) from hermes_cli.mcp_config import mcp_command mcp_command(args) - mock_run.assert_called_once_with(verbose=True) + mock_run.assert_called_once_with(verbose=True, include_tools=False) + + def test_dispatcher_routes_serve_with_tools(self, monkeypatch, tmp_path): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + mock_run = MagicMock() + monkeypatch.setattr("mcp_serve.run_mcp_server", mock_run) + + import argparse + args = argparse.Namespace(mcp_action="serve", verbose=False, tools=True) + from hermes_cli.mcp_config import mcp_command + mcp_command(args) + mock_run.assert_called_once_with(verbose=False, include_tools=True) # --------------------------------------------------------------------------- diff --git a/website/docs/user-guide/features/mcp.md b/website/docs/user-guide/features/mcp.md index 66879394b8867..6a25642b9ed91 100644 --- a/website/docs/user-guide/features/mcp.md +++ b/website/docs/user-guide/features/mcp.md @@ -871,10 +871,38 @@ The event queue is in-memory and starts when the bridge connects. Older messages ### Options ```bash -hermes mcp serve # Normal mode +hermes mcp serve # Normal mode (messaging bridge only) +hermes mcp serve --tools # Also expose Hermes' tool surface (see below) hermes mcp serve --verbose # Debug logging on stderr ``` +### Exposing Hermes' tools to other agents (`--tools`) + +With `--tools`, the server additionally registers Hermes' curated tool +surface alongside the messaging bridge: `web_search`, `web_extract`, the +`browser_*` automation suite, `vision_analyze`, `image_generate`, +`skill_view`/`skills_list`, and `text_to_speech`. Any MCP client — Claude +Code, Codex, Cursor, a custom harness — can then use Hermes' configured +connectors and capabilities directly, under the credentials configured in +your Hermes install: + +```json +{ + "mcpServers": { + "hermes": { + "command": "hermes", + "args": ["mcp", "serve", "--tools"] + } + } +} +``` + +Tools whose backend isn't configured in your Hermes install (e.g. no web +provider) are skipped automatically, matching what Hermes' own model would +see. Switching models or clients no longer means leaving your tools behind: +the same web/browser/vision/skills stack follows you into any MCP-speaking +agent. + ### How it works The MCP server reads conversation data directly from Hermes's session store — `~/.hermes/state.db` is the primary source, with `sessions.json` kept only as a legacy fallback. A background thread polls the database for new messages and maintains an in-memory event queue. For sending messages, it uses the same internal send engine (`tools/send_message_tool.py`) that powers cron delivery and the `hermes send` CLI.