How are you going to test a new MCP server? Link to heading
In many projects, testing strategies tend to swing between two extremes: heavily relying on unit tests (UT) or end-to-end (E2E) tests. However, this binary approach often overlooks a critical layer: isolated integration testing. For the MCP server, it could be beneficial to also focus on this layer to reduce testing environment complexity by removing LLM tool setup (for example, Claude Desktop) and real interaction with our system that is probably already tested.
Prerequisites Link to heading
I assume you have basic knowledge about MCP servers. If not, here is a great article
MCP server overview Link to heading
Integration tests diagram Link to heading
If you like, you can open the draw.io diagrams at the bottom of this blog entry.
Python with pytest integration tests Link to heading
Helper Function Link to heading
First, let’s define a helper function for sending JSON-RPC requests to the MCP server:
import json
import subprocess
def send_mcp_request(process, request):
"""Send JSON-RPC request to MCP server and return response"""
# Send request to process
request_str = json.dumps(request) + "\n"
process.stdin.write(request_str)
process.stdin.flush()
# Read response
response_line = process.stdout.readline()
return json.loads(response_line)
Creating fixtures Link to heading
Mock HTTP Server Link to heading
import pytest
@pytest.fixture()
def api_base_url(httpserver):
"""Set up mock HTTP server with login endpoints"""
# Mock successful login response for valid credentials
httpserver.expect_request(
'/login',
method='POST',
json={"email": "[email protected]", "password": "password123"}
).respond_with_json(
{
"success": True,
"message": "Login successful",
"token": "mock-jwt-token-12345"
},
status=200
)
# Mock unauthorized login response for invalid credentials
httpserver.expect_request(
'/login',
method='POST',
json={"email": "[email protected]", "password": "wrongpassword"}
).respond_with_json(
{
"success": False,
"message": "Invalid credentials",
"token": None
},
status=401
)
return httpserver.url_for('/')
Starting MCP server and mcp handshake fixtures Link to heading
import os
import subprocess
import pytest
@pytest.fixture()
def mcp_proc(request, api_base_url):
"""Start MCP server process with environment configuration"""
# Set environment variable for API base URL
env = os.environ.copy()
env["MCP_BASE_URL"] = api_base_url
# Start MCP server process
process = subprocess.Popen(
["python", "-m", "simple_mcp"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
text=True
)
# Cleanup function to terminate process
def cleanup():
process.terminate()
process.wait()
request.addfinalizer(cleanup)
return process
@pytest.fixture()
def mcp_initialized(mcp_proc):
"""Initialize MCP server with JSON-RPC handshake"""
# Send initialize request
initialize_request = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
}
}
response = send_mcp_request(mcp_proc, initialize_request)
# Verify initialize response
assert "result" in response
assert response["result"]["protocolVersion"] == "2024-11-05"
# Send initialized notification
initialized_notification = {
"jsonrpc": "2.0",
"method": "initialized",
"params": {}
}
send_mcp_request(mcp_proc, initialized_notification)
return mcp_proc
Test Examples Using the Fixtures Link to heading
Successful Login (200 Response) Link to heading
import json
def test_login_success_200(self, mcp_initialized, httpserver):
# Send login request to MCP server with valid credentials
login_request = {
"jsonrpc": "2.0",
"id": "test-login-success",
"method": "tools/call",
"params": {
"name": "login",
"arguments": {
"email": "[email protected]",
"password": "password123"
}
}
}
response = send_mcp_request(mcp_initialized, login_request)
# Verify HTTP request was sent to mocked server
assert len(httpserver.log) == 1
request = httpserver.log[0]
assert request.method == "POST"
assert request.url == "/login"
assert request.json == {
"email": "[email protected]",
"password": "password123"
}
# Verify response
assert "result" in response
assert response.get("error") is None # error should be None, not missing
assert "content" in response["result"]
assert len(response["result"]["content"]) >= 1
assert "text" in response["result"]["content"][0]
# Parse the response content
content_text = response["result"]["content"][0]["text"]
login_response = json.loads(content_text)
assert login_response["success"] is True
assert login_response["message"] == "Login successful"
assert login_response["token"] == "mock-jwt-token-12345"
Unauthorized Login (401 Response) Link to heading
def test_login_unauthorized_401(self, mcp_initialized):
# Send login request to MCP server with invalid credentials
expected_server_error = -32000
login_request = {
"jsonrpc": "2.0",
"id": "test-login-unauthorized",
"method": "tools/call",
"params": {
"name": "login",
"arguments": {
"email": "[email protected]",
"password": "wrongpassword"
}
}
}
response = send_mcp_request(mcp_initialized, login_request)
# Verify error response
assert response.get("error") is not None
assert response.get("result") is None
assert response["error"]["code"] == expected_server_error
assert "401" in response["error"]["message"]
Summary Link to heading
Testing MCP servers effectively requires a balanced approach that goes beyond the traditional unit vs. end-to-end testing dichotomy. By implementing isolated integration testing, we can achieve several key benefits:
Key Advantages of Integration Testing for MCP Servers Link to heading
- Reduced Complexity: Eliminates the need for full LLM tool setup (like Claude Desktop) and real system interactions
- Faster Feedback: Tests run quickly without external dependencies
- Reliable Results: Predictable test outcomes using mocked HTTP servers
- Cost Effective: No need for expensive API calls or external service dependencies
Testing Strategy Components Link to heading
The approach demonstrated in this article includes:
- Mock HTTP Server: Using
pytest-httpserver
to simulate external API responses - Process Management: Proper MCP server lifecycle management with cleanup
- JSON-RPC Protocol Testing: Complete handshake and communication testing
- Error Handling: Testing both success and failure scenarios (200 vs 401 responses)
Implementation Benefits Link to heading
This testing methodology allows developers to:
- Verify MCP server behavior in isolation
- Test JSON-RPC protocol compliance
- Validate error handling and edge cases
- Ensure proper integration with external APIs
- Maintain test reliability and speed
By focusing on this middle layer of testing, teams can build more robust MCP servers while maintaining efficient development cycles and reducing the complexity of their testing infrastructure.