diff --git a/tests/tools/test_mcp_tool.py b/tests/tools/test_mcp_tool.py index f300082ec3..3796d8ced9 100644 --- a/tests/tools/test_mcp_tool.py +++ b/tests/tools/test_mcp_tool.py @@ -2447,3 +2447,229 @@ class TestDiscoveryFailedCount: _servers.pop("ok1", None) _servers.pop("ok2", None) _servers.pop("fail1", None) + + +class TestMCPSelectiveToolLoading: + """Tests for per-server MCP filtering and utility tool policies.""" + + def _make_server(self, name, tool_names, session=None): + server = _make_mock_server( + name, + session=session or SimpleNamespace(), + tools=[_make_mcp_tool(n, n) for n in tool_names], + ) + return server + + def _run_discover(self, name, tool_names, config, session=None): + from tools.registry import ToolRegistry + from tools.mcp_tool import _discover_and_register_server, _servers + + mock_registry = ToolRegistry() + server = self._make_server(name, tool_names, session=session) + + async def fake_connect(_name, _config): + return server + + async def run(): + with patch("tools.mcp_tool._connect_server", side_effect=fake_connect), \ + patch("tools.registry.registry", mock_registry), \ + patch("toolsets.create_custom_toolset"): + return await _discover_and_register_server(name, config) + + try: + registered = asyncio.run(run()) + finally: + _servers.pop(name, None) + return registered, mock_registry + + def test_include_takes_precedence_over_exclude(self): + config = { + "url": "https://mcp.example.com", + "tools": { + "include": ["create_service"], + "exclude": ["create_service", "delete_service"], + }, + } + registered, _ = self._run_discover( + "ink", + ["create_service", "delete_service", "list_services"], + config, + session=SimpleNamespace(), + ) + assert registered == ["mcp_ink_create_service"] + + def test_exclude_filter_registers_all_except_listed_tools(self): + config = { + "url": "https://mcp.example.com", + "tools": {"exclude": ["delete_service"]}, + } + registered, _ = self._run_discover( + "ink_exclude", + ["create_service", "delete_service", "list_services"], + config, + session=SimpleNamespace(), + ) + assert registered == [ + "mcp_ink_exclude_create_service", + "mcp_ink_exclude_list_services", + ] + + def test_include_filter_skips_utility_tools_without_capabilities(self): + config = { + "url": "https://mcp.example.com", + "tools": {"include": ["create_service"]}, + } + registered, mock_registry = self._run_discover( + "ink_no_caps", + ["create_service", "delete_service"], + config, + session=SimpleNamespace(), + ) + assert registered == ["mcp_ink_no_caps_create_service"] + assert set(mock_registry.get_all_tool_names()) == {"mcp_ink_no_caps_create_service"} + + def test_no_filter_registers_all_server_tools_when_no_utilities_supported(self): + registered, _ = self._run_discover( + "ink_no_filter", + ["create_service", "delete_service", "list_services"], + {"url": "https://mcp.example.com"}, + session=SimpleNamespace(), + ) + assert registered == [ + "mcp_ink_no_filter_create_service", + "mcp_ink_no_filter_delete_service", + "mcp_ink_no_filter_list_services", + ] + + def test_resources_and_prompts_can_be_disabled_explicitly(self): + session = SimpleNamespace( + list_resources=AsyncMock(), + read_resource=AsyncMock(), + list_prompts=AsyncMock(), + get_prompt=AsyncMock(), + ) + config = { + "url": "https://mcp.example.com", + "tools": { + "resources": False, + "prompts": False, + }, + } + registered, _ = self._run_discover( + "ink_disabled_utils", + ["create_service"], + config, + session=session, + ) + assert registered == ["mcp_ink_disabled_utils_create_service"] + + def test_registers_only_utility_tools_supported_by_server_capabilities(self): + session = SimpleNamespace( + list_resources=AsyncMock(return_value=SimpleNamespace(resources=[])), + read_resource=AsyncMock(return_value=SimpleNamespace(contents=[])), + ) + registered, _ = self._run_discover( + "ink_resources_only", + ["create_service"], + {"url": "https://mcp.example.com"}, + session=session, + ) + assert "mcp_ink_resources_only_create_service" in registered + assert "mcp_ink_resources_only_list_resources" in registered + assert "mcp_ink_resources_only_read_resource" in registered + assert "mcp_ink_resources_only_list_prompts" not in registered + assert "mcp_ink_resources_only_get_prompt" not in registered + + def test_existing_tool_names_reflect_registered_subset(self): + from tools.mcp_tool import _existing_tool_names, _servers, _discover_and_register_server + from tools.registry import ToolRegistry + + mock_registry = ToolRegistry() + server = self._make_server( + "ink_existing", + ["create_service", "delete_service"], + session=SimpleNamespace(), + ) + + async def fake_connect(_name, _config): + return server + + async def run(): + with patch("tools.mcp_tool._connect_server", side_effect=fake_connect), \ + patch("tools.registry.registry", mock_registry), \ + patch("toolsets.create_custom_toolset"): + return await _discover_and_register_server( + "ink_existing", + {"url": "https://mcp.example.com", "tools": {"include": ["create_service"]}}, + ) + + try: + registered = asyncio.run(run()) + assert registered == ["mcp_ink_existing_create_service"] + assert _existing_tool_names() == ["mcp_ink_existing_create_service"] + finally: + _servers.pop("ink_existing", None) + + def test_no_toolset_created_when_everything_is_filtered_out(self): + from tools.registry import ToolRegistry + from tools.mcp_tool import _discover_and_register_server, _servers + + mock_registry = ToolRegistry() + server = self._make_server("ink_none", ["create_service"], session=SimpleNamespace()) + mock_create = MagicMock() + + async def fake_connect(_name, _config): + return server + + async def run(): + with patch("tools.mcp_tool._connect_server", side_effect=fake_connect), \ + patch("tools.registry.registry", mock_registry), \ + patch("toolsets.create_custom_toolset", mock_create): + return await _discover_and_register_server( + "ink_none", + { + "url": "https://mcp.example.com", + "tools": { + "include": ["missing_tool"], + "resources": False, + "prompts": False, + }, + }, + ) + + try: + registered = asyncio.run(run()) + assert registered == [] + mock_create.assert_not_called() + assert mock_registry.get_all_tool_names() == [] + finally: + _servers.pop("ink_none", None) + + def test_enabled_false_skips_connection_attempt(self): + from tools.mcp_tool import discover_mcp_tools + + connect_called = [] + + async def fake_connect(name, config): + connect_called.append(name) + return self._make_server(name, ["create_service"]) + + fake_config = { + "ink": { + "url": "https://mcp.example.com", + "enabled": False, + } + } + fake_toolsets = { + "hermes-cli": {"tools": [], "description": "CLI", "includes": []}, + } + + with patch("tools.mcp_tool._MCP_AVAILABLE", True), \ + patch("tools.mcp_tool._servers", {}), \ + patch("tools.mcp_tool._load_mcp_config", return_value=fake_config), \ + patch("tools.mcp_tool._connect_server", side_effect=fake_connect), \ + patch("toolsets.TOOLSETS", fake_toolsets): + result = discover_mcp_tools() + + assert connect_called == [] + assert result == [] diff --git a/tools/mcp_tool.py b/tools/mcp_tool.py index 448af9202a..7294e8be56 100644 --- a/tools/mcp_tool.py +++ b/tools/mcp_tool.py @@ -688,7 +688,7 @@ class MCPServerTask: __slots__ = ( "name", "session", "tool_timeout", "_task", "_ready", "_shutdown_event", "_tools", "_error", "_config", - "_sampling", + "_sampling", "_registered_tool_names", ) def __init__(self, name: str): @@ -702,6 +702,7 @@ class MCPServerTask: self._error: Optional[Exception] = None self._config: dict = {} self._sampling: Optional[SamplingHandler] = None + self._registered_tool_names: list[str] = [] def _is_http(self) -> bool: """Check if this server uses HTTP transport.""" @@ -1308,16 +1309,81 @@ def _build_utility_schemas(server_name: str) -> List[dict]: ] +def _normalize_name_filter(value: Any, label: str) -> set[str]: + """Normalize include/exclude config to a set of tool names.""" + if value is None: + return set() + if isinstance(value, str): + return {value} + if isinstance(value, (list, tuple, set)): + return {str(item) for item in value} + logger.warning("MCP config %s must be a string or list of strings; ignoring %r", label, value) + return set() + + +def _parse_boolish(value: Any, default: bool = True) -> bool: + """Parse a bool-like config value with safe fallback.""" + if value is None: + return default + if isinstance(value, bool): + return value + if isinstance(value, str): + lowered = value.strip().lower() + if lowered in {"true", "1", "yes", "on"}: + return True + if lowered in {"false", "0", "no", "off"}: + return False + logger.warning("MCP config expected a boolean-ish value, got %r; using default=%s", value, default) + return default + + +_UTILITY_CAPABILITY_METHODS = { + "list_resources": "list_resources", + "read_resource": "read_resource", + "list_prompts": "list_prompts", + "get_prompt": "get_prompt", +} + + +def _select_utility_schemas(server_name: str, server: MCPServerTask, config: dict) -> List[dict]: + """Select utility schemas based on config and server capabilities.""" + tools_filter = config.get("tools") or {} + resources_enabled = _parse_boolish(tools_filter.get("resources"), default=True) + prompts_enabled = _parse_boolish(tools_filter.get("prompts"), default=True) + + selected: List[dict] = [] + for entry in _build_utility_schemas(server_name): + handler_key = entry["handler_key"] + if handler_key in {"list_resources", "read_resource"} and not resources_enabled: + logger.debug("MCP server '%s': skipping utility '%s' (resources disabled)", server_name, handler_key) + continue + if handler_key in {"list_prompts", "get_prompt"} and not prompts_enabled: + logger.debug("MCP server '%s': skipping utility '%s' (prompts disabled)", server_name, handler_key) + continue + + required_method = _UTILITY_CAPABILITY_METHODS[handler_key] + if not hasattr(server.session, required_method): + logger.debug( + "MCP server '%s': skipping utility '%s' (session lacks %s)", + server_name, + handler_key, + required_method, + ) + continue + selected.append(entry) + return selected + + def _existing_tool_names() -> List[str]: """Return tool names for all currently connected servers.""" names: List[str] = [] - for sname, server in _servers.items(): + for _sname, server in _servers.items(): + if hasattr(server, "_registered_tool_names"): + names.extend(server._registered_tool_names) + continue for mcp_tool in server._tools: - schema = _convert_mcp_schema(sname, mcp_tool) + schema = _convert_mcp_schema(server.name, mcp_tool) names.append(schema["name"]) - # Also include utility tool names - for entry in _build_utility_schemas(sname): - names.append(entry["schema"]["name"]) return names @@ -1343,7 +1409,27 @@ async def _discover_and_register_server(name: str, config: dict) -> List[str]: registered_names: List[str] = [] toolset_name = f"mcp-{name}" + # Selective tool loading: honour include/exclude lists from config. + # Rules (matching issue #690 spec): + # tools.include — whitelist: only these tool names are registered + # tools.exclude — blacklist: all tools EXCEPT these are registered + # include takes precedence over exclude + # Neither set → register all tools (backward-compatible default) + tools_filter = config.get("tools") or {} + include_set = _normalize_name_filter(tools_filter.get("include"), f"mcp_servers.{name}.tools.include") + exclude_set = _normalize_name_filter(tools_filter.get("exclude"), f"mcp_servers.{name}.tools.exclude") + + def _should_register(tool_name: str) -> bool: + if include_set: + return tool_name in include_set + if exclude_set: + return tool_name not in exclude_set + return True + for mcp_tool in server._tools: + if not _should_register(mcp_tool.name): + logger.debug("MCP server '%s': skipping tool '%s' (filtered by config)", name, mcp_tool.name) + continue schema = _convert_mcp_schema(name, mcp_tool) tool_name_prefixed = schema["name"] @@ -1358,7 +1444,8 @@ async def _discover_and_register_server(name: str, config: dict) -> List[str]: ) registered_names.append(tool_name_prefixed) - # Register MCP Resources & Prompts utility tools + # Register MCP Resources & Prompts utility tools, filtered by config and + # only when the server actually supports the corresponding capability. _handler_factories = { "list_resources": _make_list_resources_handler, "read_resource": _make_read_resource_handler, @@ -1366,7 +1453,7 @@ async def _discover_and_register_server(name: str, config: dict) -> List[str]: "get_prompt": _make_get_prompt_handler, } check_fn = _make_check_fn(name) - for entry in _build_utility_schemas(name): + for entry in _select_utility_schemas(name, server, config): schema = entry["schema"] handler_key = entry["handler_key"] handler = _handler_factories[handler_key](name, server.tool_timeout) @@ -1382,6 +1469,8 @@ async def _discover_and_register_server(name: str, config: dict) -> List[str]: ) registered_names.append(schema["name"]) + server._registered_tool_names = list(registered_names) + # Create a custom toolset so these tools are discoverable if registered_names: create_custom_toolset( @@ -1424,9 +1513,14 @@ def discover_mcp_tools() -> List[str]: logger.debug("No MCP servers configured") return [] - # Only attempt servers that aren't already connected + # Only attempt servers that aren't already connected and are enabled + # (enabled: false skips the server entirely without removing its config) with _lock: - new_servers = {k: v for k, v in servers.items() if k not in _servers} + new_servers = { + k: v + for k, v in servers.items() + if k not in _servers and _parse_boolish(v.get("enabled", True), default=True) + } if not new_servers: return _existing_tool_names() @@ -1513,7 +1607,7 @@ def get_mcp_status() -> List[dict]: entry = { "name": name, "transport": transport, - "tools": len(server._tools), + "tools": len(server._registered_tool_names) if hasattr(server, "_registered_tool_names") else len(server._tools), "connected": True, } if server._sampling: diff --git a/website/docs/guides/use-mcp-with-hermes.md b/website/docs/guides/use-mcp-with-hermes.md new file mode 100644 index 0000000000..e202594d18 --- /dev/null +++ b/website/docs/guides/use-mcp-with-hermes.md @@ -0,0 +1,410 @@ +--- +sidebar_position: 5 +title: "Use MCP with Hermes" +description: "A practical guide to connecting MCP servers to Hermes Agent, filtering their tools, and using them safely in real workflows" +--- + +# Use MCP with Hermes + +This guide shows how to actually use MCP with Hermes Agent in day-to-day workflows. + +If the feature page explains what MCP is, this guide is about how to get value from it quickly and safely. + +## When should you use MCP? + +Use MCP when: +- a tool already exists in MCP form and you do not want to build a native Hermes tool +- you want Hermes to operate against a local or remote system through a clean RPC layer +- you want fine-grained per-server exposure control +- you want to connect Hermes to internal APIs, databases, or company systems without modifying Hermes core + +Do not use MCP when: +- a built-in Hermes tool already solves the job well +- the server exposes a huge dangerous tool surface and you are not prepared to filter it +- you only need one very narrow integration and a native tool would be simpler and safer + +## Mental model + +Think of MCP as an adapter layer: + +- Hermes remains the agent +- MCP servers contribute tools +- Hermes discovers those tools at startup or reload time +- the model can use them like normal tools +- you control how much of each server is visible + +That last part matters. Good MCP usage is not just “connect everything.” It is “connect the right thing, with the smallest useful surface.” + +## Step 1: install MCP support + +```bash +pip install hermes-agent[mcp] +``` + +For npm-based servers, make sure Node.js and `npx` are available. + +For many Python MCP servers, `uvx` is a nice default. + +## Step 2: add one server first + +Start with a single, safe server. + +Example: filesystem access to one project directory only. + +```yaml +mcp_servers: + project_fs: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/my-project"] +``` + +Then start Hermes: + +```bash +hermes chat +``` + +Now ask something concrete: + +```text +Inspect this project and summarize the repo layout. +``` + +## Step 3: verify MCP loaded + +You can verify MCP in a few ways: + +- Hermes banner/status should show MCP integration when configured +- ask Hermes what tools it has available +- use `/reload-mcp` after config changes +- check logs if the server failed to connect + +A practical test prompt: + +```text +Tell me which MCP-backed tools are available right now. +``` + +## Step 4: start filtering immediately + +Do not wait until later if the server exposes a lot of tools. + +### Example: whitelist only what you want + +```yaml +mcp_servers: + github: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-github"] + env: + GITHUB_PERSONAL_ACCESS_TOKEN: "***" + tools: + include: [list_issues, create_issue, search_code] +``` + +This is usually the best default for sensitive systems. + +### Example: blacklist dangerous actions + +```yaml +mcp_servers: + stripe: + url: "https://mcp.stripe.com" + headers: + Authorization: "Bearer ***" + tools: + exclude: [delete_customer, refund_payment] +``` + +### Example: disable utility wrappers too + +```yaml +mcp_servers: + docs: + url: "https://mcp.docs.example.com" + tools: + prompts: false + resources: false +``` + +## What does filtering actually affect? + +There are two categories of MCP-exposed functionality in Hermes: + +1. Server-native MCP tools +- filtered with: + - `tools.include` + - `tools.exclude` + +2. Hermes-added utility wrappers +- filtered with: + - `tools.resources` + - `tools.prompts` + +### Utility wrappers you may see + +Resources: +- `list_resources` +- `read_resource` + +Prompts: +- `list_prompts` +- `get_prompt` + +These wrappers only appear if: +- your config allows them, and +- the MCP server session actually supports those capabilities + +So Hermes will not pretend a server has resources/prompts if it does not. + +## Common patterns + +### Pattern 1: local project assistant + +Use MCP for a repo-local filesystem or git server when you want Hermes to reason over a bounded workspace. + +```yaml +mcp_servers: + fs: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/project"] + + git: + command: "uvx" + args: ["mcp-server-git", "--repository", "/home/user/project"] +``` + +Good prompts: + +```text +Review the project structure and identify where configuration lives. +``` + +```text +Check the local git state and summarize what changed recently. +``` + +### Pattern 2: GitHub triage assistant + +```yaml +mcp_servers: + github: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-github"] + env: + GITHUB_PERSONAL_ACCESS_TOKEN: "***" + tools: + include: [list_issues, create_issue, update_issue, search_code] + prompts: false + resources: false +``` + +Good prompts: + +```text +List open issues about MCP, cluster them by theme, and draft a high-quality issue for the most common bug. +``` + +```text +Search the repo for uses of _discover_and_register_server and explain how MCP tools are registered. +``` + +### Pattern 3: internal API assistant + +```yaml +mcp_servers: + internal_api: + url: "https://mcp.internal.example.com" + headers: + Authorization: "Bearer ***" + tools: + include: [list_customers, get_customer, list_invoices] + resources: false + prompts: false +``` + +Good prompts: + +```text +Look up customer ACME Corp and summarize recent invoice activity. +``` + +This is the sort of place where a strict whitelist is far better than an exclude list. + +### Pattern 4: documentation / knowledge servers + +Some MCP servers expose prompts or resources that are more like shared knowledge assets than direct actions. + +```yaml +mcp_servers: + docs: + url: "https://mcp.docs.example.com" + tools: + prompts: true + resources: true +``` + +Good prompts: + +```text +List available MCP resources from the docs server, then read the onboarding guide and summarize it. +``` + +```text +List prompts exposed by the docs server and tell me which ones would help with incident response. +``` + +## Tutorial: end-to-end setup with filtering + +Here is a practical progression. + +### Phase 1: add GitHub MCP with a tight whitelist + +```yaml +mcp_servers: + github: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-github"] + env: + GITHUB_PERSONAL_ACCESS_TOKEN: "***" + tools: + include: [list_issues, create_issue, search_code] + prompts: false + resources: false +``` + +Start Hermes and ask: + +```text +Search the codebase for references to MCP and summarize the main integration points. +``` + +### Phase 2: expand only when needed + +If you later need issue updates too: + +```yaml +tools: + include: [list_issues, create_issue, update_issue, search_code] +``` + +Then reload: + +```text +/reload-mcp +``` + +### Phase 3: add a second server with different policy + +```yaml +mcp_servers: + github: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-github"] + env: + GITHUB_PERSONAL_ACCESS_TOKEN: "***" + tools: + include: [list_issues, create_issue, update_issue, search_code] + prompts: false + resources: false + + filesystem: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/project"] +``` + +Now Hermes can combine them: + +```text +Inspect the local project files, then create a GitHub issue summarizing the bug you find. +``` + +That is where MCP gets powerful: multi-system workflows without changing Hermes core. + +## Safe usage recommendations + +### Prefer allowlists for dangerous systems + +For anything financial, customer-facing, or destructive: +- use `tools.include` +- start with the smallest set possible + +### Disable unused utilities + +If you do not want the model browsing server-provided resources/prompts, turn them off: + +```yaml +tools: + resources: false + prompts: false +``` + +### Keep servers scoped narrowly + +Examples: +- filesystem server rooted to one project dir, not your whole home directory +- git server pointed at one repo +- internal API server with read-heavy tool exposure by default + +### Reload after config changes + +```text +/reload-mcp +``` + +Do this after changing: +- include/exclude lists +- enabled flags +- resources/prompts toggles +- auth headers / env + +## Troubleshooting by symptom + +### "The server connects but the tools I expected are missing" + +Possible causes: +- filtered by `tools.include` +- excluded by `tools.exclude` +- utility wrappers disabled via `resources: false` or `prompts: false` +- server does not actually support resources/prompts + +### "The server is configured but nothing loads" + +Check: +- `enabled: false` was not left in config +- command/runtime exists (`npx`, `uvx`, etc.) +- HTTP endpoint is reachable +- auth env or headers are correct + +### "Why do I see fewer tools than the MCP server advertises?" + +Because Hermes now respects your per-server policy and capability-aware registration. That is expected, and usually desirable. + +### "How do I remove an MCP server without deleting the config?" + +Use: + +```yaml +enabled: false +``` + +That keeps the config around but prevents connection and registration. + +## Recommended first MCP setups + +Good first servers for most users: +- filesystem +- git +- GitHub +- fetch / documentation MCP servers +- one narrow internal API + +Not-great first servers: +- giant business systems with lots of destructive actions and no filtering +- anything you do not understand well enough to constrain + +## Related docs + +- [MCP (Model Context Protocol)](/docs/user-guide/features/mcp) +- [FAQ](/docs/reference/faq) +- [Slash Commands](/docs/reference/slash-commands) diff --git a/website/docs/index.md b/website/docs/index.md index a4ea0a8e38..0e33c9dc30 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -31,7 +31,8 @@ It's not a coding copilot tethered to an IDE or a chatbot wrapper around a singl | 🔧 **[Tools & Toolsets](/docs/user-guide/features/tools)** | 40+ built-in tools and how to configure them | | 🧠 **[Memory System](/docs/user-guide/features/memory)** | Persistent memory that grows across sessions | | 📚 **[Skills System](/docs/user-guide/features/skills)** | Procedural memory the agent creates and reuses | -| 🔌 **[MCP Integration](/docs/user-guide/features/mcp)** | Connect to any MCP server for extended capabilities | +| 🔌 **[MCP Integration](/docs/user-guide/features/mcp)** | Connect to MCP servers, filter their tools, and extend Hermes safely | +| 🧭 **[Use MCP with Hermes](/docs/guides/use-mcp-with-hermes)** | Practical MCP setup patterns, examples, and tutorials | | 📄 **[Context Files](/docs/user-guide/features/context-files)** | Project context files that shape every conversation | | 🔒 **[Security](/docs/user-guide/security)** | Command approval, authorization, container isolation | | 💡 **[Tips & Best Practices](/docs/guides/tips)** | Quick wins to get the most out of Hermes | diff --git a/website/docs/reference/faq.md b/website/docs/reference/faq.md index 88e5210a28..02a82dce7e 100644 --- a/website/docs/reference/faq.md +++ b/website/docs/reference/faq.md @@ -391,21 +391,28 @@ mcp_servers: #### Tools not showing up from MCP server -**Cause:** Server started but tool discovery failed, or tools are filtered out. +**Cause:** Server started but tool discovery failed, tools were filtered out by config, or the server does not support the MCP capability you expected. **Solution:** - Check gateway/agent logs for MCP connection errors - Ensure the server responds to the `tools/list` RPC method -- Restart the agent — MCP tools are discovered at startup +- Review any `tools.include`, `tools.exclude`, `tools.resources`, `tools.prompts`, or `enabled` settings under that server +- Remember that resource/prompt utility tools are only registered when the session actually supports those capabilities +- Use `/reload-mcp` after changing config ```bash # Verify MCP servers are configured -hermes config show | grep -A 5 mcp_servers +hermes config show | grep -A 12 mcp_servers -# Restart hermes to re-discover tools +# Restart Hermes or reload MCP after config changes hermes chat ``` +See also: +- [MCP (Model Context Protocol)](/docs/user-guide/features/mcp) +- [Use MCP with Hermes](/docs/guides/use-mcp-with-hermes) +- [MCP Config Reference](/docs/reference/mcp-config-reference) + #### MCP timeout errors **Cause:** The MCP server is taking too long to respond, or it crashed during execution. diff --git a/website/docs/reference/mcp-config-reference.md b/website/docs/reference/mcp-config-reference.md new file mode 100644 index 0000000000..5f78185b9d --- /dev/null +++ b/website/docs/reference/mcp-config-reference.md @@ -0,0 +1,215 @@ +--- +sidebar_position: 8 +title: "MCP Config Reference" +description: "Reference for Hermes Agent MCP configuration keys, filtering semantics, and utility-tool policy" +--- + +# MCP Config Reference + +This page is the compact reference companion to the main MCP docs. + +For conceptual guidance, see: +- [MCP (Model Context Protocol)](/docs/user-guide/features/mcp) +- [Use MCP with Hermes](/docs/guides/use-mcp-with-hermes) + +## Root config shape + +```yaml +mcp_servers: + : + command: "..." # stdio servers + args: [] + env: {} + + # OR + url: "..." # HTTP servers + headers: {} + + enabled: true + timeout: 120 + connect_timeout: 60 + tools: + include: [] + exclude: [] + resources: true + prompts: true +``` + +## Server keys + +| Key | Type | Applies to | Meaning | +|---|---|---|---| +| `command` | string | stdio | Executable to launch | +| `args` | list | stdio | Arguments for the subprocess | +| `env` | mapping | stdio | Environment passed to the subprocess | +| `url` | string | HTTP | Remote MCP endpoint | +| `headers` | mapping | HTTP | Headers for remote server requests | +| `enabled` | bool | both | Skip the server entirely when false | +| `timeout` | number | both | Tool call timeout | +| `connect_timeout` | number | both | Initial connection timeout | +| `tools` | mapping | both | Filtering and utility-tool policy | + +## `tools` policy keys + +| Key | Type | Meaning | +|---|---|---| +| `include` | string or list | Whitelist server-native MCP tools | +| `exclude` | string or list | Blacklist server-native MCP tools | +| `resources` | bool-like | Enable/disable `list_resources` + `read_resource` | +| `prompts` | bool-like | Enable/disable `list_prompts` + `get_prompt` | + +## Filtering semantics + +### `include` + +If `include` is set, only those server-native MCP tools are registered. + +```yaml +tools: + include: [create_issue, list_issues] +``` + +### `exclude` + +If `exclude` is set and `include` is not, every server-native MCP tool except those names is registered. + +```yaml +tools: + exclude: [delete_customer] +``` + +### Precedence + +If both are set, `include` wins. + +```yaml +tools: + include: [create_issue] + exclude: [create_issue, delete_issue] +``` + +Result: +- `create_issue` is still allowed +- `delete_issue` is ignored because `include` takes precedence + +## Utility-tool policy + +Hermes may register these utility wrappers per MCP server: + +Resources: +- `list_resources` +- `read_resource` + +Prompts: +- `list_prompts` +- `get_prompt` + +### Disable resources + +```yaml +tools: + resources: false +``` + +### Disable prompts + +```yaml +tools: + prompts: false +``` + +### Capability-aware registration + +Even when `resources: true` or `prompts: true`, Hermes only registers those utility tools if the MCP session actually exposes the corresponding capability. + +So this is normal: +- you enable prompts +- but no prompt utilities appear +- because the server does not support prompts + +## `enabled: false` + +```yaml +mcp_servers: + legacy: + url: "https://mcp.legacy.internal" + enabled: false +``` + +Behavior: +- no connection attempt +- no discovery +- no tool registration +- config remains in place for later reuse + +## Empty result behavior + +If filtering removes all server-native tools and no utility tools are registered, Hermes does not create an empty MCP runtime toolset for that server. + +## Example configs + +### Safe GitHub allowlist + +```yaml +mcp_servers: + github: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-github"] + env: + GITHUB_PERSONAL_ACCESS_TOKEN: "***" + tools: + include: [list_issues, create_issue, update_issue, search_code] + resources: false + prompts: false +``` + +### Stripe blacklist + +```yaml +mcp_servers: + stripe: + url: "https://mcp.stripe.com" + headers: + Authorization: "Bearer ***" + tools: + exclude: [delete_customer, refund_payment] +``` + +### Resource-only docs server + +```yaml +mcp_servers: + docs: + url: "https://mcp.docs.example.com" + tools: + include: [] + resources: true + prompts: false +``` + +## Reloading config + +After changing MCP config, reload servers with: + +```text +/reload-mcp +``` + +## Tool naming + +Server-native MCP tools become: + +```text +mcp__ +``` + +Examples: +- `mcp_github_create_issue` +- `mcp_filesystem_read_file` +- `mcp_my_api_query_data` + +Utility tools follow the same prefixing pattern: +- `mcp__list_resources` +- `mcp__read_resource` +- `mcp__list_prompts` +- `mcp__get_prompt` diff --git a/website/docs/user-guide/features/mcp.md b/website/docs/user-guide/features/mcp.md index d1caeb0656..5009fab70a 100644 --- a/website/docs/user-guide/features/mcp.md +++ b/website/docs/user-guide/features/mcp.md @@ -1,334 +1,408 @@ --- sidebar_position: 4 title: "MCP (Model Context Protocol)" -description: "Connect Hermes Agent to external tool servers via MCP — databases, APIs, filesystems, and more" +description: "Connect Hermes Agent to external tool servers via MCP — and control exactly which MCP tools Hermes loads" --- # MCP (Model Context Protocol) -MCP lets Hermes Agent connect to external tool servers — giving the agent access to databases, APIs, filesystems, and more without any code changes. +MCP lets Hermes Agent connect to external tool servers so the agent can use tools that live outside Hermes itself — GitHub, databases, file systems, browser stacks, internal APIs, and more. -## Overview +If you have ever wanted Hermes to use a tool that already exists somewhere else, MCP is usually the cleanest way to do it. -The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an open standard for connecting AI agents to external tools and data sources. MCP servers expose tools over a lightweight RPC protocol, and Hermes Agent can connect to any compliant server automatically. +## What MCP gives you -What this means for you: +- Access to external tool ecosystems without writing a native Hermes tool first +- Local stdio servers and remote HTTP MCP servers in the same config +- Automatic tool discovery and registration at startup +- Utility wrappers for MCP resources and prompts when supported by the server +- Per-server filtering so you can expose only the MCP tools you actually want Hermes to see -- **Thousands of ready-made tools** — browse the [MCP server directory](https://github.com/modelcontextprotocol/servers) for servers covering GitHub, Slack, databases, file systems, web scraping, and more -- **No code changes needed** — add a few lines to `~/.hermes/config.yaml` and the tools appear alongside built-in ones -- **Mix and match** — run multiple MCP servers simultaneously, combining stdio-based and HTTP-based servers -- **Secure by default** — environment variables are filtered and credentials are stripped from error messages +## Quick start -## Prerequisites +1. Install MCP support: ```bash pip install hermes-agent[mcp] ``` -| Server Type | Runtime Needed | Example | -|-------------|---------------|---------| -| HTTP/remote | Nothing extra | `url: "https://mcp.example.com"` | -| npm-based (npx) | Node.js 18+ | `command: "npx"` | -| Python-based | uv (recommended) | `command: "uvx"` | - -## Configuration - -MCP servers are configured in `~/.hermes/config.yaml` under the `mcp_servers` key. - -### Stdio Servers - -Stdio servers run as local subprocesses, communicating over stdin/stdout: +2. Add an MCP server to `~/.hermes/config.yaml`: ```yaml mcp_servers: filesystem: command: "npx" args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"] - env: {} +``` +3. Start Hermes: + +```bash +hermes chat +``` + +4. Ask Hermes to use the MCP-backed capability. + +For example: + +```text +List the files in /home/user/projects and summarize the repo structure. +``` + +Hermes will discover the MCP server's tools and use them like any other tool. + +## Two kinds of MCP servers + +### Stdio servers + +Stdio servers run as local subprocesses and talk over stdin/stdout. + +```yaml +mcp_servers: github: command: "npx" args: ["-y", "@modelcontextprotocol/server-github"] env: - GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_xxxxxxxxxxxx" + GITHUB_PERSONAL_ACCESS_TOKEN: "***" ``` -| Key | Required | Description | -|-----|----------|-------------| -| `command` | Yes | Executable to run (`npx`, `uvx`, `python`) | -| `args` | No | Command-line arguments | -| `env` | No | Environment variables for the subprocess | +Use stdio servers when: +- the server is installed locally +- you want low-latency access to local resources +- you are following MCP server docs that show `command`, `args`, and `env` -:::info Security -Only explicitly listed `env` variables plus a safe baseline (`PATH`, `HOME`, `USER`, `LANG`, `SHELL`, `TMPDIR`, `XDG_*`) are passed to the subprocess. Your API keys and secrets are **not** leaked. -::: +### HTTP servers -### HTTP Servers +HTTP MCP servers are remote endpoints Hermes connects to directly. ```yaml mcp_servers: remote_api: - url: "https://my-mcp-server.example.com/mcp" + url: "https://mcp.example.com/mcp" headers: - Authorization: "Bearer sk-xxxxxxxxxxxx" + Authorization: "Bearer ***" ``` -### Per-Server Timeouts +Use HTTP servers when: +- the MCP server is hosted elsewhere +- your organization exposes internal MCP endpoints +- you do not want Hermes spawning a local subprocess for that integration + +## Basic configuration reference + +Hermes reads MCP config from `~/.hermes/config.yaml` under `mcp_servers`. + +### Common keys + +| Key | Type | Meaning | +|---|---|---| +| `command` | string | Executable for a stdio MCP server | +| `args` | list | Arguments for the stdio server | +| `env` | mapping | Environment variables passed to the stdio server | +| `url` | string | HTTP MCP endpoint | +| `headers` | mapping | HTTP headers for remote servers | +| `timeout` | number | Tool call timeout | +| `connect_timeout` | number | Initial connection timeout | +| `enabled` | bool | If `false`, Hermes skips the server entirely | +| `tools` | mapping | Per-server tool filtering and utility policy | + +### Minimal stdio example ```yaml mcp_servers: - slow_database: - command: "npx" - args: ["-y", "@modelcontextprotocol/server-postgres"] - env: - DATABASE_URL: "postgres://user:pass@localhost/mydb" - timeout: 300 # Tool call timeout (default: 120s) - connect_timeout: 90 # Initial connection timeout (default: 60s) -``` - -### Mixed Configuration Example - -```yaml -mcp_servers: - # Local filesystem via stdio - filesystem: - command: "npx" - args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] - - # GitHub API via stdio with auth - github: - command: "npx" - args: ["-y", "@modelcontextprotocol/server-github"] - env: - GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_xxxxxxxxxxxx" - - # Remote database via HTTP - company_db: - url: "https://mcp.internal.company.com/db" - headers: - Authorization: "Bearer sk-xxxxxxxxxxxx" - timeout: 180 - - # Python-based server via uvx - memory: - command: "uvx" - args: ["mcp-server-memory"] -``` - -## Translating from Claude Desktop Config - -Many MCP server docs show Claude Desktop JSON format. Here's the translation: - -**Claude Desktop JSON:** -```json -{ - "mcpServers": { - "filesystem": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] - } - } -} -``` - -**Hermes YAML:** -```yaml -mcp_servers: # mcpServers → mcp_servers (snake_case) filesystem: command: "npx" args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] ``` -Rules: `mcpServers` → `mcp_servers` (snake_case), JSON → YAML. Keys like `command`, `args`, `env` are identical. +### Minimal HTTP example -## How It Works - -### Tool Registration - -Each MCP tool is registered with a prefixed name: - -``` -mcp_{server_name}_{tool_name} +```yaml +mcp_servers: + company_api: + url: "https://mcp.internal.example.com" + headers: + Authorization: "Bearer ***" ``` -| Server Name | MCP Tool Name | Registered As | -|-------------|--------------|---------------| +## How Hermes registers MCP tools + +Hermes prefixes MCP tools so they do not collide with built-in names: + +```text +mcp__ +``` + +Examples: + +| Server | MCP tool | Registered name | +|---|---|---| | `filesystem` | `read_file` | `mcp_filesystem_read_file` | | `github` | `create-issue` | `mcp_github_create_issue` | | `my-api` | `query.data` | `mcp_my_api_query_data` | -Tools appear alongside built-in tools — the agent calls them like any other tool. +In practice, you usually do not need to call the prefixed name manually — Hermes sees the tool and chooses it during normal reasoning. -:::info -In addition to the server's own tools, each MCP server also gets 4 utility tools auto-registered: `list_resources`, `read_resource`, `list_prompts`, and `get_prompt`. These allow the agent to discover and use MCP resources and prompts exposed by the server. +## MCP utility tools -Each configured server also creates a **runtime toolset** named `mcp-`. This means you can filter or reason about MCP servers at the toolset level in the same way you do with built-in toolsets. -::: +When supported, Hermes also registers utility tools around MCP resources and prompts: -### Reconnection +- `list_resources` +- `read_resource` +- `list_prompts` +- `get_prompt` -If an MCP server disconnects, Hermes automatically reconnects with exponential backoff (1s, 2s, 4s, 8s, 16s — max 5 attempts). Initial connection failures are reported immediately. +These are registered per server with the same prefix pattern, for example: -### Shutdown +- `mcp_github_list_resources` +- `mcp_github_get_prompt` -On agent exit, all MCP server connections are cleanly shut down. +### Important -## Popular MCP Servers +These utility tools are now capability-aware: +- Hermes only registers resource utilities if the MCP session actually supports resource operations +- Hermes only registers prompt utilities if the MCP session actually supports prompt operations -| Server | Package | Description | -|--------|---------|-------------| -| Filesystem | `@modelcontextprotocol/server-filesystem` | Read/write/search local files | -| GitHub | `@modelcontextprotocol/server-github` | Issues, PRs, repos, code search | -| Git | `@modelcontextprotocol/server-git` | Git operations on local repos | -| Fetch | `@modelcontextprotocol/server-fetch` | HTTP fetching and web content | -| Memory | `@modelcontextprotocol/server-memory` | Persistent key-value memory | -| SQLite | `@modelcontextprotocol/server-sqlite` | Query SQLite databases | -| PostgreSQL | `@modelcontextprotocol/server-postgres` | Query PostgreSQL databases | -| Brave Search | `@modelcontextprotocol/server-brave-search` | Web search via Brave API | -| Puppeteer | `@modelcontextprotocol/server-puppeteer` | Browser automation | +So a server that exposes callable tools but no resources/prompts will not get those extra wrappers. -### Example Configs +## Per-server filtering + +This is the main feature added by the PR work. + +You can now control which tools each MCP server contributes to Hermes. + +### Disable a server entirely ```yaml mcp_servers: - # No API key needed - filesystem: - command: "npx" - args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"] + legacy: + url: "https://mcp.legacy.internal" + enabled: false +``` - git: - command: "uvx" - args: ["mcp-server-git", "--repository", "/home/user/my-repo"] +If `enabled: false`, Hermes skips the server completely and does not even attempt a connection. - fetch: - command: "uvx" - args: ["mcp-server-fetch"] +### Whitelist server tools - sqlite: - command: "uvx" - args: ["mcp-server-sqlite", "--db-path", "/home/user/data.db"] - - # Requires API key +```yaml +mcp_servers: github: command: "npx" args: ["-y", "@modelcontextprotocol/server-github"] env: - GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_xxxxxxxxxxxx" + GITHUB_PERSONAL_ACCESS_TOKEN: "***" + tools: + include: [create_issue, list_issues] +``` - brave_search: +Only those MCP server tools are registered. + +### Blacklist server tools + +```yaml +mcp_servers: + stripe: + url: "https://mcp.stripe.com" + tools: + exclude: [delete_customer] +``` + +All server tools are registered except the excluded ones. + +### Precedence rule + +If both are present: + +```yaml +tools: + include: [create_issue] + exclude: [create_issue, delete_issue] +``` + +`include` wins. + +### Filter utility tools too + +You can also separately disable Hermes-added utility wrappers: + +```yaml +mcp_servers: + docs: + url: "https://mcp.docs.example.com" + tools: + prompts: false + resources: false +``` + +That means: +- `tools.resources: false` disables `list_resources` and `read_resource` +- `tools.prompts: false` disables `list_prompts` and `get_prompt` + +### Full example + +```yaml +mcp_servers: + github: command: "npx" - args: ["-y", "@modelcontextprotocol/server-brave-search"] + args: ["-y", "@modelcontextprotocol/server-github"] env: - BRAVE_API_KEY: "BSA_xxxxxxxxxxxx" + GITHUB_PERSONAL_ACCESS_TOKEN: "***" + tools: + include: [create_issue, list_issues, search_code] + prompts: false + + stripe: + url: "https://mcp.stripe.com" + headers: + Authorization: "Bearer ***" + tools: + exclude: [delete_customer] + resources: false + + legacy: + url: "https://mcp.legacy.internal" + enabled: false +``` + +## What happens if everything is filtered out? + +If your config filters out all callable tools and disables or omits all supported utilities, Hermes does not create an empty runtime MCP toolset for that server. + +That keeps the tool list clean. + +## Runtime behavior + +### Discovery time + +Hermes discovers MCP servers at startup and registers their tools into the normal tool registry. + +### Reloading + +If you change MCP config, use: + +```text +/reload-mcp +``` + +This reloads MCP servers from config and refreshes the available tool list. + +### Toolsets + +Each configured MCP server also creates a runtime toolset when it contributes at least one registered tool: + +```text +mcp- +``` + +That makes MCP servers easier to reason about at the toolset level. + +## Security model + +### Stdio env filtering + +For stdio servers, Hermes does not blindly pass your full shell environment. + +Only explicitly configured `env` plus a safe baseline are passed through. This reduces accidental secret leakage. + +### Config-level exposure control + +The new filtering support is also a security control: +- disable dangerous tools you do not want the model to see +- expose only a minimal whitelist for a sensitive server +- disable resource/prompt wrappers when you do not want that surface exposed + +## Example use cases + +### GitHub server with a minimal issue-management surface + +```yaml +mcp_servers: + github: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-github"] + env: + GITHUB_PERSONAL_ACCESS_TOKEN: "***" + tools: + include: [list_issues, create_issue, update_issue] + prompts: false + resources: false +``` + +Use it like: + +```text +Show me open issues labeled bug, then draft a new issue for the flaky MCP reconnection behavior. +``` + +### Stripe server with dangerous actions removed + +```yaml +mcp_servers: + stripe: + url: "https://mcp.stripe.com" + headers: + Authorization: "Bearer ***" + tools: + exclude: [delete_customer, refund_payment] +``` + +Use it like: + +```text +Look up the last 10 failed payments and summarize common failure reasons. +``` + +### Filesystem server for a single project root + +```yaml +mcp_servers: + project_fs: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/my-project"] +``` + +Use it like: + +```text +Inspect the project root and explain the directory layout. ``` ## Troubleshooting -### "MCP SDK not available" +### MCP server not connecting + +Check: ```bash pip install hermes-agent[mcp] +node --version +npx --version ``` -### Server fails to start +Then verify your config and restart Hermes. -The MCP server command (`npx`, `uvx`) is not on PATH. Install the required runtime: +### Tools not appearing -```bash -# For npm-based servers -npm install -g npx # or ensure Node.js 18+ is installed +Possible causes: +- the server failed to connect +- discovery failed +- your filter config excluded the tools +- the utility capability does not exist on that server +- the server is disabled with `enabled: false` -# For Python-based servers -pip install uv # then use "uvx" as the command -``` +If you are intentionally filtering, this is expected. -### Server connects but tools fail with auth errors +### Why didn't resource or prompt utilities appear? -Ensure the key is in the server's `env` block: +Because Hermes now only registers those wrappers when both are true: +1. your config allows them +2. the server session actually supports the capability -```yaml -mcp_servers: - github: - command: "npx" - args: ["-y", "@modelcontextprotocol/server-github"] - env: - GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_your_actual_token" # Check this -``` +This is intentional and keeps the tool list honest. -### Connection timeout +## Related docs -Increase `connect_timeout` for slow-starting servers: - -```yaml -mcp_servers: - slow_server: - command: "npx" - args: ["-y", "heavy-server-package"] - connect_timeout: 120 # default is 60 -``` - -### Reload MCP Servers - -You can reload MCP servers without restarting Hermes: - -- In the CLI: the agent reconnects automatically -- In messaging: send `/reload-mcp` - -## Sampling (Server-Initiated LLM Requests) - -MCP's `sampling/createMessage` capability allows MCP servers to request LLM completions through the Hermes agent. This enables agent-in-the-loop workflows where servers can leverage the LLM during tool execution — for example, a database server asking the LLM to interpret query results, or a code analysis server requesting the LLM to review findings. - -### How It Works - -When an MCP server sends a `sampling/createMessage` request: - -1. The sampling callback validates against rate limits and model whitelist -2. Resolves which model to use (config override > server hint > default) -3. Converts MCP messages to OpenAI-compatible format -4. Offloads the LLM call to a thread via `asyncio.to_thread()` (non-blocking) -5. Returns the response (text or tool use) back to the server - -### Configuration - -Sampling is **enabled by default** for all MCP servers. No extra setup needed — if you have an auxiliary LLM client configured, sampling works automatically. - -```yaml -mcp_servers: - analysis_server: - command: "npx" - args: ["-y", "my-analysis-server"] - sampling: - enabled: true # default: true - model: "gemini-3-flash" # override model (optional) - max_tokens_cap: 4096 # max tokens per request (default: 4096) - timeout: 30 # LLM call timeout in seconds (default: 30) - max_rpm: 10 # max requests per minute (default: 10) - allowed_models: [] # model whitelist (empty = allow all) - max_tool_rounds: 5 # max consecutive tool use rounds (0 = disable) - log_level: "info" # audit verbosity: debug, info, warning -``` - -### Tool Use in Sampling - -Servers can include `tools` and `toolChoice` in sampling requests, enabling multi-turn tool-augmented workflows within a single sampling session. The callback forwards tool definitions to the LLM, handles tool use responses with proper `ToolUseContent` types, and enforces `max_tool_rounds` to prevent infinite loops. - -### Security - -- **Rate limiting**: Per-server sliding window (default: 10 req/min) -- **Token cap**: Servers can't request more than `max_tokens_cap` (default: 4096) -- **Model whitelist**: `allowed_models` restricts which models a server can use -- **Tool loop limit**: `max_tool_rounds` caps consecutive tool use rounds -- **Credential stripping**: LLM responses are sanitized before returning to the server -- **Non-blocking**: LLM calls run in a separate thread via `asyncio.to_thread()` -- **Typed errors**: All failures return structured `ErrorData` per MCP spec - -To disable sampling for untrusted servers: - -```yaml -mcp_servers: - untrusted: - command: "npx" - args: ["-y", "untrusted-server"] - sampling: - enabled: false -``` +- [Use MCP with Hermes](/docs/guides/use-mcp-with-hermes) +- [CLI Commands](/docs/reference/cli-commands) +- [Slash Commands](/docs/reference/slash-commands) +- [FAQ](/docs/reference/faq) diff --git a/website/sidebars.ts b/website/sidebars.ts index 0861cdf082..21b20b315e 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -22,6 +22,7 @@ const sidebars: SidebarsConfig = { 'guides/daily-briefing-bot', 'guides/team-telegram-assistant', 'guides/python-library', + 'guides/use-mcp-with-hermes', ], }, { @@ -128,6 +129,7 @@ const sidebars: SidebarsConfig = { 'reference/slash-commands', 'reference/tools-reference', 'reference/toolsets-reference', + 'reference/mcp-config-reference', 'reference/skills-catalog', 'reference/optional-skills-catalog', 'reference/environment-variables',