Skip to main content

A pytest plugin for testing MCP (Model Context Protocol) servers

Project description

pytest-mcp

A pytest plugin for testing MCP (Model Context Protocol) servers.

PyPI version Python versions License: MIT Tests

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 tools
  • async call_tool(name: str, arguments: dict) -> CallToolResult - Execute a tool
  • async list_resources() -> list[Resource] - List available resources
  • async read_resource(uri: str) -> ReadResourceResult - Read a resource
  • async get_tool(name: str) -> Tool | None - Get specific tool by name

Fixtures

  • mcp_client - Auto-injected client connected to your server
  • mcp_server - User-defined fixture that returns server parameters
  • mcp_test_server - Advanced fixture with lifecycle control
  • snapshot - Snapshot testing helper
  • mcp_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

Resources

Community

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcp_test_framework-0.1.0.tar.gz (23.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcp_test_framework-0.1.0-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

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

Hashes for mcp_test_framework-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8ed1b23e2289bc95e2b0b6bbb22f0fabbf25a1160037035ddb17f3f02b0c9bc6
MD5 9cfd345cc3feb3a9efe7c9c2a49be1da
BLAKE2b-256 1aae9c8aff97e791a6d172a9c91c750e4d918a663d54d07018928e1a4781d05d

See more details on using hashes here.

File details

Details for the file mcp_test_framework-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for mcp_test_framework-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 759ff8ff88630db067cc0cf24d57ec21d7a15d776b50299eb84031be2062b7fb
MD5 437677019ea019fe9cb3e483086840bc
BLAKE2b-256 310436334ab7702273b62ccf159b05836ece65023e13028197b5847324f9b1bc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page