mcp-test-framework
A pytest plugin for testing MCP (Model Context Protocol) servers.
Quick Start
pip install mcp-test-framework
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
Metadata
Release files for mcp-test-framework 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| mcp_test_framework-0.1.1.tar.gz | 23.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_test_framework-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 43.8 kB
Release files / mcp_test_framework-0.1.1.tar.gz
| Download URL | mcp_test_framework-0.1.1.tar.gz |
|---|---|
| Size | 23.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8277679c4cad8493b88d58d40deb8b55875d4127161eca636357834f8051ee75
|
|
BLAKE2b-256 checksum How to use checksums |
2dcbdf25ce81ef97c44cfcc843925b5b61428465a7a106ea614d51b30c61e862
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Release files / mcp_test_framework-0.1.1-py3-none-any.whl
| Download URL | mcp_test_framework-0.1.1-py3-none-any.whl |
|---|---|
| Size | 20.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b95993e788391e93ebea65aed504f7db0930b721e6e11e0dd1e115283708c833
|
|
BLAKE2b-256 checksum How to use checksums |
b8a8f315f4cdc5a32f36759462221f255120c441ad955a1b9a8baef11766bd5a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Security for mcp-test-framework 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.