A pytest plugin for testing MCP (Model Context Protocol) servers
Project description
pytest-mcp
A pytest plugin for testing MCP (Model Context Protocol) servers.
Quick Start
pip install pytest-mcp
import pytest
from mcp import StdioServerParameters
from pytest_mcp import assert_tool_exists
@pytest.fixture
def mcp_server():
return StdioServerParameters(
command="python", args=["my_server.py"]
)
async def test_my_tool(mcp_client):
await assert_tool_exists(mcp_client, "my_tool")
result = await mcp_client.call_tool("my_tool", {"arg": "value"})
assert result is not None
The plugin automatically handles server lifecycle, connection management, and provides rich assertions.
Features
Mock MCP Client
Test servers in-process without network overhead:
from pytest_mcp import MockMCPClient
async with MockMCPClient(command="python", args=["server.py"]) as client:
tools = await client.list_tools()
result = await client.call_tool("add", {"a": 1, "b": 2})
Auto-Injected Fixtures
Define your server fixture and get a connected client automatically:
@pytest.fixture
def mcp_server():
return {"command": "python", "args": ["server.py"]}
async def test_tool(mcp_client):
tools = await mcp_client.list_tools()
assert len(tools) > 0
Rich Assertions
Use descriptive assertions designed for MCP testing:
from pytest_mcp import (
assert_tool_exists,
assert_tool_output_matches,
assert_tool_returns_error,
assert_resource_exists,
)
async def test_calculator(mcp_client):
await assert_tool_exists(mcp_client, "add")
result = await mcp_client.call_tool("add", {"a": 2, "b": 3})
await assert_tool_output_matches(result, 5)
await assert_tool_returns_error(
mcp_client, "divide", {"a": 1, "b": 0},
error_message="division by zero"
)
await assert_resource_exists(mcp_client, "config://settings")
Snapshot Testing
Save and compare tool outputs across test runs:
async def test_user_data(mcp_client, snapshot):
result = await mcp_client.call_tool("get_user", {"id": 1})
snapshot.assert_match(result, "user_1_response")
Update snapshots when needed:
pytest --mcp-update-snapshots
Server Lifecycle Management
Control server startup and shutdown for integration tests:
from pytest_mcp import MCPTestServer
async def test_integration():
async with MCPTestServer("python", ["server.py"]) as server:
client = server.get_client()
result = await client.call_tool("hello", {"name": "world"})
await server.restart()
API Reference
Client
MockMCPClient
MockMCPClient(
server_params: StdioServerParameters | None = None,
*,
command: str | None = None,
args: Sequence[str] | None = None,
env: dict[str, str] | None = None,
)
Methods:
async list_tools() -> list[Tool]- List available toolsasync call_tool(name: str, arguments: dict) -> CallToolResult- Execute a toolasync list_resources() -> list[Resource]- List available resourcesasync read_resource(uri: str) -> ReadResourceResult- Read a resourceasync get_tool(name: str) -> Tool | None- Get specific tool by name
Fixtures
mcp_client- Auto-injected client connected to your servermcp_server- User-defined fixture that returns server parametersmcp_test_server- Advanced fixture with lifecycle controlsnapshot- Snapshot testing helpermcp_server_env- Environment variables for server
Assertions
# Tool assertions
await assert_tool_exists(client, "tool_name")
await assert_tool_count(client, expected_count)
await assert_tool_output_matches(result, expected_value, partial=False)
await assert_tool_returns_error(client, "tool_name", args, error_message="...")
await assert_tools_have_unique_names(client)
# Schema validation
assert_tool_schema_valid(tool)
# Resource assertions
await assert_resource_exists(client, "resource://uri")
await assert_resource_content_matches(client, "resource://uri", expected_content)
Snapshot Testing
# JSON snapshots
snapshot.assert_match(data, "snapshot_name")
snapshot.assert_match_json({"key": "value"}, "json_snapshot")
# Text snapshots
snapshot.assert_match_text("output", "text_snapshot")
# Utilities
snapshot.get_snapshot("name")
snapshot.delete_snapshot("name")
snapshot.list_snapshots()
Server Management
async with MCPTestServer(command, args, env) as server:
client = server.get_client()
await server.restart()
await server.wait_for_ready()
Usage Examples
Basic Calculator Server
server.py:
from mcp.server import Server
from mcp.types import Tool, TextContent
app = Server("calculator")
@app.list_tools()
async def list_tools():
return [
Tool(
name="add",
description="Add two numbers",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"},
},
"required": ["a", "b"],
},
)
]
@app.call_tool()
async def call_tool(name, arguments):
if name == "add":
result = arguments["a"] + arguments["b"]
return [TextContent(type="text", text=str(result))]
test_server.py:
import pytest
from pytest_mcp import assert_tool_exists, assert_tool_output_matches
@pytest.fixture
def mcp_server():
return {"command": "python", "args": ["server.py"]}
async def test_add(mcp_client):
await assert_tool_exists(mcp_client, "add")
result = await mcp_client.call_tool("add", {"a": 5, "b": 3})
await assert_tool_output_matches(result, "8")
Advanced Features
Testing Resources:
async def test_resources(mcp_client):
resources = await mcp_client.list_resources()
assert len(resources) > 0
content = await mcp_client.read_resource("config://settings")
assert content is not None
Error Handling:
async def test_validation(mcp_client):
await assert_tool_returns_error(
mcp_client,
"divide",
{"a": 10, "b": 0},
error_message="Cannot divide by zero"
)
Snapshot Testing:
async def test_complex_output(mcp_client, snapshot):
result = await mcp_client.call_tool("get_report", {"id": 123})
snapshot.assert_match(result, "report_123")
Configuration
pytest.ini / pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
markers = [
"mcp: MCP server test (auto-applied)",
"mcp_integration: MCP integration test",
"mcp_slow: Slow MCP test",
]
Command-Line Options
# Set log level
pytest --mcp-log-level=DEBUG
# Set operation timeout
pytest --mcp-timeout=60
# Update snapshots
pytest --mcp-update-snapshots
Integration with FastMCP
Works with FastMCP:
from fastmcp import FastMCP
from pytest_mcp import MockMCPClient
mcp = FastMCP("My Server")
@mcp.tool()
def greet(name: str) -> str:
return f"Hello, {name}!"
@pytest.fixture
def mcp_server():
return mcp.get_server_params()
async def test_greet(mcp_client):
result = await mcp_client.call_tool("greet", {"name": "Alice"})
await assert_tool_output_matches(result, "Hello, Alice!")
Contributing
Contributions are welcome. To get started:
git clone https://github.com/aryanjp1/pytest-mcp.git
cd pytest-mcp
pip install -e ".[dev]"
pytest
black .
ruff check .
mypy src/
See CONTRIBUTING.md for detailed guidelines.
License
MIT License - see LICENSE file.
Acknowledgments
- Built for the Model Context Protocol by Anthropic
- Inspired by pytest plugin architecture
- Uses the mcp Python SDK
Resources
Community
- GitHub Issues - Bug reports and feature requests
- Discussions - Questions and ideas
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_test_framework-0.1.0.tar.gz.
File metadata
- Download URL: mcp_test_framework-0.1.0.tar.gz
- Upload date:
- Size: 23.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ed1b23e2289bc95e2b0b6bbb22f0fabbf25a1160037035ddb17f3f02b0c9bc6
|
|
| MD5 |
9cfd345cc3feb3a9efe7c9c2a49be1da
|
|
| BLAKE2b-256 |
1aae9c8aff97e791a6d172a9c91c750e4d918a663d54d07018928e1a4781d05d
|
File details
Details for the file mcp_test_framework-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mcp_test_framework-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
759ff8ff88630db067cc0cf24d57ec21d7a15d776b50299eb84031be2062b7fb
|
|
| MD5 |
437677019ea019fe9cb3e483086840bc
|
|
| BLAKE2b-256 |
310436334ab7702273b62ccf159b05836ece65023e13028197b5847324f9b1bc
|