Write your first MCP test
This walkthrough starts a small MCP server as a local process, discovers its shipping_quote tool, calls it, and checks the structured response. You will also make the assertion fail, restore it, and open the saved run.
Requirements
- Python 3.10 or newer.
- uv and the standalone M3 CLI.
- No agent provider key, M3 account, or external server.
Install the CLI once with uv:
uv tool install sf-m3-cliOn macOS or Linux, you can use the shell installer instead:
curl -LsSf https://m3.sineframe.com/install.sh | shCreate a project, install the matching Python SDK, and check the environment:
mkdir shipping-test
cd shipping-test
m3 init
m3 setup
m3 doctorWhen m3 init asks for a project name and suite name, enter shipping-test and shipping. m3 setup installs the SDK and pytest support into the project's Python environment. It does not install the CLI or edit your dependency manifest or lockfile. The CLI and SDK are separate installations; keep them on matching releases. See install and update M3.
Add the server and test
Create shipping_server.py beside the tests directory:
"""A deterministic MCP shipping quote server used by the first-test guide."""
from __future__ import annotations
import anyio
from mcp import types
from mcp.server.lowlevel import Server
from mcp.server.stdio import stdio_server
async def list_tools(_context: object, _params: object) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="shipping_quote",
description="Calculate a deterministic shipping quote",
input_schema={
"type": "object",
"properties": {
"weight_kg": {"type": "number", "exclusiveMinimum": 0},
"zone": {
"type": "string",
"enum": ["local", "regional", "international"],
},
},
"required": ["weight_kg", "zone"],
"additionalProperties": False,
},
)
]
)
async def call_tool(
_context: object, params: types.CallToolRequestParams
) -> types.CallToolResult:
arguments = params.arguments or {}
rates = {"local": 2.0, "regional": 3.5, "international": 7.0}
amount = round(5 + float(arguments["weight_kg"]) * rates[str(arguments["zone"])], 2)
quote = {"amount": amount, "currency": "USD"}
return types.CallToolResult(
content=[types.TextContent(text=f"{amount:.2f} USD")],
structured_content=quote,
)
async def main() -> None:
server = Server(
"shipping-server",
version="1.0.0",
on_list_tools=list_tools,
on_call_tool=call_tool,
)
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream, write_stream, server.create_initialization_options()
)
if __name__ == "__main__":
anyio.run(main)Replace the generated skipped test at tests/test_m3_starter.py with this complete file:
import sys
from pathlib import Path
import pytest
from m3 import MCPTestKit, StdioServer
pytestmark = pytest.mark.m3(suite_name="shipping")
def test_shipping_quote() -> None:
"""shipping_quote prices a 2 kg local parcel at 9.00 USD."""
project_root = Path(__file__).parents[1]
server = StdioServer(
name="shipping",
command=sys.executable,
args=(str(project_root / "shipping_server.py"),),
cwd=str(project_root),
)
with MCPTestKit(env={}) as kit, kit.direct(server) as client:
tools = client.list_all_tools()
assert [tool.name for tool in tools] == ["shipping_quote"]
result = client.call_tool("shipping_quote", {"weight_kg": 2, "zone": "local"})
assert result.is_error is False
assert result.structured_content == {"amount": 9.0, "currency": "USD"}The StdioServer command uses the same Python environment as pytest and points at the server file relative to the test. The context managers close the MCP connection and the subprocess after the assertion has its result.
Run the test
From the project root, run:
m3 test -- tests/test_m3_starter.pyPytest should report one passing test. The assertion checks both that discovery returned the intended tool and that a 2 kg local quote has the structured value {"amount": 9.0, "currency": "USD"}. A successful protocol call alone would not prove that the server returned the right quote.
See a real failure
Change the expected amount from 9.0 to 10.0 and rerun the same command. The test should fail at the structured-content assertion and show the actual 9.0 result. Restore 9.0 and rerun; the test should pass again.
Open the saved run
From the project root, open the history created by the test:
m3 uiM3 opens the saved-run list in a local browser. Select the run from the test you completed. The CLI stores history in .m3/executions.sqlite by default. See inspect a saved run for navigating the report.
To test a server you already have, continue to test your own server.