feat: hermes mcp serve --tools — expose Hermes' tool surface to any MCP client
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
This commit is contained in:
parent
03fa32c92d
commit
60938d5bab
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
39
mcp_serve.py
39
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue