Testing MCP Servers: Mock Clients and Schema Validation
Build robust unit and integration test suites for Model Context Protocol (MCP) servers using in-memory mock clients, JSON Schema validation, and pytest.
Published on • September 11, 2026
AI Assistant

Developing reliable MCP tools requires thorough automated testing. Because AI agents rely on exact JSON Schema definitions and structured responses to make reasoning decisions, an untested tool that returns invalid JSON or uncaught exceptions will crash agent execution.
To ensure production stability, engineering teams need automated test suites that validate MCP Schema Definitions, JSON-RPC Handshakes, and Tool Handler Outputs using mock clients.
Testing Layers for MCP Servers
A comprehensive testing strategy covers three layers:
- Schema Validation Tests: Asserting that generated JSON Schema parameters match expected types and descriptions.
- Tool Logic Unit Tests: Executing tool handler functions directly in isolation with mocked backend dependencies.
- End-to-End MCP Integration Tests: Spinning up an in-memory MCP server and connecting a client session to verify full request/response roundtrips over JSON-RPC.
[Pytest Suite]
|-- Unit Tests (Direct Function Call)
|-- Schema Tests (JSON Schema Validation)
|-- Integration Tests (In-Memory Stdio Client -> FastMCP Server)
Unit & Schema Testing with Pytest and FastMCP
Using pytest and pytest-asyncio, we construct isolated unit tests for an MCP server:
import pytest
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
# Define target MCP server
mcp = FastMCP("Testable Server")
class CalculationInput(BaseModel):
a: float = Field(..., description="First operand")
b: float = Field(..., description="Second operand")
@mcp.tool()
async def add_numbers(params: CalculationInput) -> str:
"""Adds two numeric operands"""
result = params.a + params.b
return f"Sum: {result}"
# ---------------------------------------------------------
# Test Cases
# ---------------------------------------------------------
@pytest.mark.asyncio
async def test_tool_schema_registration():
"""Verify tool is correctly registered with valid schema"""
tools = await mcp.list_tools()
tool_names = [t.name for t in tools]
assert "add_numbers" in tool_names
target_tool = next(t for t in tools if t.name == "add_numbers")
assert target_tool.description == "Adds two numeric operands"
assert "a" in target_tool.inputSchema["properties"]
assert "b" in target_tool.inputSchema["properties"]
@pytest.mark.asyncio
async def test_tool_execution_logic():
"""Direct execution test of underlying tool function"""
input_data = CalculationInput(a=12.5, b=7.5)
output = await add_numbers(input_data)
assert output == "Sum: 20.0"
Integration Testing with In-Memory Mock Clients
To test the entire network stack (deserialization, routing, response packaging), spin up an in-memory stdio connection inside pytest fixtures:
import pytest_asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
@pytest_asyncio.fixture
async def mcp_client_session():
"""Fixture that boots the MCP server in a subprocess and yields a client session"""
server_params = StdioServerParameters(
command="python",
args=["-m", "my_mcp_server_module"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
yield session
@pytest.mark.asyncio
async def test_mcp_client_roundtrip(mcp_client_session):
"""End-to-end client invocation test"""
result = await mcp_client_session.call_tool("add_numbers", {"a": 10, "b": 20})
assert len(result.content) > 0
assert result.content[0].text == "Sum: 30.0"
Best Practices for MCP Test Automation
- Automate CI Schema Checks: Run schema validation tests on every pull request to catch accidental field removal or description changes.
- Mock External Side Effects: Use
unittest.mockorhttpx.MockTransportto mock external API calls or database connections inside tool handlers during unit tests. - Assert Error Response Handling: Verify that tools return clean human-readable error messages rather than unhandled Python tracebacks when invalid parameters are provided.
For comprehensive test patterns, client interfaces, and protocol tools, refer to the official Model Context Protocol Documentation.