"""Tests for external MCP server functionality."""
import json
import subprocess
from unittest.mock import Mock, patch
from mcp_chain.external import ExternalMCPServer
def test_external_mcp_server_creation():
"""Test creating an ExternalMCPServer with a server name."""
server = ExternalMCPServer("test-server")
assert server.name == "test-server"
@patch("subprocess.Popen")
def test_external_mcp_server_has_get_metadata(mock_popen):
"""Test that ExternalMCPServer has get_metadata method."""
# Setup mock process
mock_process = Mock()
mock_process.stdin = Mock()
mock_process.stdout = Mock()
mock_process.poll.return_value = None
mock_popen.return_value = mock_process
# Mock responses
init_response = {"jsonrpc": "2.0", "id": 1, "result": {}}
tools_response = {"jsonrpc": "2.0", "id": 2, "result": {"tools": []}}
mock_process.stdout.readline.side_effect = [
json.dumps(init_response) + "\n",
json.dumps(tools_response) + "\n",
]
server = ExternalMCPServer("test-server", "echo", ["hello"])
metadata = server.get_metadata()
assert isinstance(metadata, dict)
@patch("subprocess.Popen")
def test_external_mcp_server_has_handle_request(mock_popen):
"""Test that ExternalMCPServer has handle_request method."""
# Setup mock process
mock_process = Mock()
mock_process.stdin = Mock()
mock_process.stdout = Mock()
mock_process.poll.return_value = None
mock_popen.return_value = mock_process
# Mock responses
init_response = {"jsonrpc": "2.0", "id": 1, "result": {}}
request_response = {"jsonrpc": "2.0", "id": 2, "result": {}}
mock_process.stdout.readline.side_effect = [
json.dumps(init_response) + "\n",
json.dumps(request_response) + "\n",
]
server = ExternalMCPServer("test-server", "echo", ["hello"])
response = server.handle_request({"method": "test"})
assert isinstance(response, dict)
def test_external_mcp_server_takes_command_and_args():
"""Test that ExternalMCPServer constructor takes command and args."""
server = ExternalMCPServer("test-server", "echo", ["hello", "world"])
assert server.command == "echo"
assert server.args == ["hello", "world"]
@patch("subprocess.Popen")
def test_external_mcp_server_connects_to_process(mock_popen):
"""Test that ExternalMCPServer can connect to external process."""
# Setup mock process
mock_process = Mock()
mock_process.stdin = Mock()
mock_process.stdout = Mock()
mock_process.poll.return_value = None
mock_popen.return_value = mock_process
# Mock initialize response
init_response = {"jsonrpc": "2.0", "id": 1, "result": {}}
mock_process.stdout.readline.return_value = json.dumps(init_response) + "\n"
server = ExternalMCPServer("test-server", "echo", ["hello"])
server.connect()
# Should call Popen with correct command
mock_popen.assert_called_once_with(
["echo", "hello"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
@patch("subprocess.Popen")
def test_external_mcp_server_get_metadata_calls_tools_list(mock_popen):
"""Test that get_metadata calls tools/list on external server."""
# Setup mock process
mock_process = Mock()
mock_process.stdin = Mock()
mock_process.stdout = Mock()
mock_process.poll.return_value = None
mock_popen.return_value = mock_process
# Mock responses
init_response = {"jsonrpc": "2.0", "id": 1, "result": {}}
tools_response = {
"jsonrpc": "2.0",
"id": 2,
"result": {"tools": [{"name": "test_tool", "description": "A test tool"}]},
}
mock_process.stdout.readline.side_effect = [
json.dumps(init_response) + "\n",
json.dumps(tools_response) + "\n",
]
server = ExternalMCPServer("test-server", "echo", ["hello"])
metadata = server.get_metadata()
assert "tools" in metadata
assert len(metadata["tools"]) == 1
assert metadata["tools"][0]["name"] == "test_tool"
@patch("subprocess.Popen")
def test_external_mcp_server_handle_request_forwards_to_external(mock_popen):
"""Test that handle_request forwards requests to external server."""
# Setup mock process
mock_process = Mock()
mock_process.stdin = Mock()
mock_process.stdout = Mock()
mock_process.poll.return_value = None
mock_popen.return_value = mock_process
# Mock responses
init_response = {"jsonrpc": "2.0", "id": 1, "result": {}}
request_response = {"jsonrpc": "2.0", "id": 3, "result": {"success": True}}
mock_process.stdout.readline.side_effect = [
json.dumps(init_response) + "\n",
json.dumps(request_response) + "\n",
]
server = ExternalMCPServer("test-server", "echo", ["hello"])
test_request = {"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {}}
response = server.handle_request(test_request)
assert response["jsonrpc"] == "2.0"
assert response["id"] == 3
assert response["result"]["success"]
@patch("subprocess.Popen")
def test_external_mcp_server_handles_invalid_json(mock_popen):
"""Test that ExternalMCPServer handles invalid JSON gracefully."""
# Setup mock process
mock_process = Mock()
mock_process.stdin = Mock()
mock_process.stdout = Mock()
mock_process.poll.return_value = None
mock_popen.return_value = mock_process
# Mock init response and error response for invalid request
init_response = {"jsonrpc": "2.0", "id": 1, "result": {}}
error_response = {
"jsonrpc": "2.0",
"id": 1,
"error": {"code": -32700, "message": "Parse error"},
}
mock_process.stdout.readline.side_effect = [
json.dumps(init_response) + "\n",
json.dumps(error_response) + "\n",
]
server = ExternalMCPServer("test-server", "echo", ["hello"])
# Test is no longer valid since we now pass dicts, not JSON strings
# Instead test with invalid dict structure
response = server.handle_request({"invalid": "structure"})
assert "error" in response
assert response["error"]["code"] == -32700
def test_external_mcp_server_has_disconnect():
"""Test that ExternalMCPServer has disconnect method."""
server = ExternalMCPServer("test-server", "echo", ["hello"])
# Should not raise an error
server.disconnect()
@patch("subprocess.Popen")
def test_external_mcp_server_can_be_used_in_chain(mock_popen):
"""Test that ExternalMCPServer can be used in mcp_chain."""
from mcp_chain import mcp_chain
# Setup mock process
mock_process = Mock()
mock_process.stdin = Mock()
mock_process.stdout = Mock()
mock_process.poll.return_value = None
mock_popen.return_value = mock_process
# Mock responses
init_response = {"jsonrpc": "2.0", "id": 1, "result": {}}
tools_response = {"jsonrpc": "2.0", "id": 2, "result": {"tools": []}}
mock_process.stdout.readline.side_effect = [
json.dumps(init_response) + "\n",
json.dumps(tools_response) + "\n",
]
# Create external server
external_server = ExternalMCPServer("test-server", "echo", ["hello"])
# Should be able to use in chain
chain = mcp_chain().then(external_server)
# Should be able to call get_metadata
metadata = chain.get_metadata()
assert isinstance(metadata, dict)