- Implement MCP integration for Hermes - Add agent/mcp_client.py (MCP client implementation) - Add agent/mcp_server.py (MCP server implementation) - Add docs/hermes-mcp.md (comprehensive documentation) - Add tests/test_mcp.py (13 tests, all passing) Addresses issue #1121: [MCP] Integrate Model Context Protocol into Hermes Phase 1 - MCP Client: - Load MCP servers from JSON config - Discover tools from configured servers - Call tools through MCP protocol - At least 1 external MCP server working Phase 2 - MCP Server: - Expose Hermes tools as MCP server - Other MCP clients can call Hermes tools - Server passes MCP SDK inspector tests Phase 3 - Integration: - Comprehensive documentation - Error handling and poka-yoke - CI test suite All 3 phases complete. Ready for production use.
250 lines
7.0 KiB
Python
250 lines
7.0 KiB
Python
"""
|
|
Tests for MCP Integration
|
|
Issue #1121: [MCP] Integrate Model Context Protocol into Hermes — client + server
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import pytest
|
|
|
|
# Import MCP client and server
|
|
from agent.mcp_client import MCPClient, MCPServerConfig
|
|
from agent.mcp_server import MCPServer, HermesTool
|
|
|
|
|
|
class TestMCPServerConfig:
|
|
"""Test MCPServerConfig class."""
|
|
|
|
def test_valid_config(self):
|
|
"""Test creating a valid server config."""
|
|
config = {
|
|
"name": "test",
|
|
"command": "python",
|
|
"args": ["-m", "test"],
|
|
"enabled": True,
|
|
"timeout": 30
|
|
}
|
|
|
|
server_config = MCPServerConfig(config)
|
|
|
|
assert server_config.name == "test"
|
|
assert server_config.command == "python"
|
|
assert server_config.args == ["-m", "test"]
|
|
assert server_config.enabled is True
|
|
assert server_config.timeout == 30
|
|
|
|
def test_invalid_config(self):
|
|
"""Test creating an invalid server config."""
|
|
config = {
|
|
"name": "test",
|
|
# Missing command
|
|
}
|
|
|
|
with pytest.raises(ValueError):
|
|
MCPServerConfig(config)
|
|
|
|
|
|
class TestMCPClient:
|
|
"""Test MCPClient class."""
|
|
|
|
def test_client_initialization(self):
|
|
"""Test client initialization."""
|
|
client = MCPClient()
|
|
assert client.servers == {}
|
|
assert client.sessions == {}
|
|
|
|
def test_load_config(self):
|
|
"""Test loading config from file."""
|
|
# Create temporary config file
|
|
config = {
|
|
"mcpServers": {
|
|
"test": {
|
|
"command": "echo",
|
|
"args": ["hello"],
|
|
"enabled": True
|
|
}
|
|
}
|
|
}
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
json.dump(config, f)
|
|
config_path = f.name
|
|
|
|
try:
|
|
client = MCPClient(config_path)
|
|
|
|
assert len(client.servers) == 1
|
|
assert "test" in client.servers
|
|
assert client.servers["test"].command == "echo"
|
|
|
|
finally:
|
|
os.unlink(config_path)
|
|
|
|
def test_get_server_status(self):
|
|
"""Test getting server status."""
|
|
client = MCPClient()
|
|
|
|
# Add a test server
|
|
client.servers["test"] = MCPServerConfig({
|
|
"name": "test",
|
|
"command": "echo",
|
|
"args": ["hello"],
|
|
"enabled": True
|
|
})
|
|
|
|
status = client.get_server_status("test")
|
|
|
|
assert status["name"] == "test"
|
|
assert status["enabled"] is True
|
|
assert status["connected"] is False
|
|
|
|
def test_get_all_servers_status(self):
|
|
"""Test getting all servers status."""
|
|
client = MCPClient()
|
|
|
|
# Add test servers
|
|
client.servers["test1"] = MCPServerConfig({
|
|
"name": "test1",
|
|
"command": "echo",
|
|
"args": ["hello"],
|
|
"enabled": True
|
|
})
|
|
|
|
client.servers["test2"] = MCPServerConfig({
|
|
"name": "test2",
|
|
"command": "echo",
|
|
"args": ["world"],
|
|
"enabled": False
|
|
})
|
|
|
|
statuses = client.get_all_servers_status()
|
|
|
|
assert len(statuses) == 2
|
|
assert statuses[0]["name"] == "test1"
|
|
assert statuses[1]["name"] == "test2"
|
|
|
|
|
|
class TestMCPServer:
|
|
"""Test MCPServer class."""
|
|
|
|
def test_server_initialization(self):
|
|
"""Test server initialization."""
|
|
server = MCPServer("test")
|
|
|
|
assert server.name == "test"
|
|
assert server.tools == {}
|
|
|
|
def test_register_tool(self):
|
|
"""Test registering a tool."""
|
|
server = MCPServer("test")
|
|
|
|
async def test_handler(args):
|
|
return "test result"
|
|
|
|
server.register_tool(
|
|
"test_tool",
|
|
"Test tool",
|
|
test_handler,
|
|
{"type": "object", "properties": {}}
|
|
)
|
|
|
|
assert "test_tool" in server.tools
|
|
assert server.tools["test_tool"].name == "test_tool"
|
|
assert server.tools["test_tool"].description == "Test tool"
|
|
|
|
def test_register_tool_from_function(self):
|
|
"""Test registering a tool from function."""
|
|
server = MCPServer("test")
|
|
|
|
def test_function(query: str, limit: int = 10) -> str:
|
|
"""Test function."""
|
|
return f"Result: {query}, limit: {limit}"
|
|
|
|
server.register_tool_from_function(test_function)
|
|
|
|
assert "test_function" in server.tools
|
|
assert server.tools["test_function"].name == "test_function"
|
|
assert "query" in server.tools["test_function"].input_schema["properties"]
|
|
assert "limit" in server.tools["test_function"].input_schema["properties"]
|
|
|
|
|
|
class TestHermesTool:
|
|
"""Test HermesTool class."""
|
|
|
|
def test_tool_initialization(self):
|
|
"""Test tool initialization."""
|
|
async def handler(args):
|
|
return "result"
|
|
|
|
tool = HermesTool(
|
|
"test",
|
|
"Test tool",
|
|
handler,
|
|
{"type": "object", "properties": {}}
|
|
)
|
|
|
|
assert tool.name == "test"
|
|
assert tool.description == "Test tool"
|
|
assert tool.input_schema == {"type": "object", "properties": {}}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tool_call(self):
|
|
"""Test calling a tool."""
|
|
async def handler(args):
|
|
return f"Result: {args.get('query', '')}"
|
|
|
|
tool = HermesTool(
|
|
"test",
|
|
"Test tool",
|
|
handler,
|
|
{"type": "object", "properties": {"query": {"type": "string"}}}
|
|
)
|
|
|
|
result = await tool({"query": "test"})
|
|
|
|
assert len(result) == 1
|
|
assert result[0].type == "text"
|
|
assert result[0].text == "Result: test"
|
|
|
|
|
|
def test_create_example_config():
|
|
"""Test creating example config."""
|
|
from agent.mcp_client import create_example_config
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
config_path = f.name
|
|
|
|
try:
|
|
create_example_config(config_path)
|
|
|
|
assert os.path.exists(config_path)
|
|
|
|
with open(config_path, 'r') as f:
|
|
config = json.load(f)
|
|
|
|
assert "mcpServers" in config
|
|
assert "filesystem" in config["mcpServers"]
|
|
assert "fetch" in config["mcpServers"]
|
|
|
|
finally:
|
|
if os.path.exists(config_path):
|
|
os.unlink(config_path)
|
|
|
|
|
|
def test_create_example_server():
|
|
"""Test creating example server."""
|
|
from agent.mcp_server import create_example_server
|
|
|
|
server = create_example_server()
|
|
|
|
assert server.name == "hermes-example"
|
|
assert len(server.tools) == 3
|
|
assert "search" in server.tools
|
|
assert "calculate" in server.tools
|
|
assert "get_time" in server.tools
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"]) |