How to Test a FastMCP Server: In-Memory Client and Pytest
Testing a FastMCP server means running it against a real MCP client without leaving your test suite: for unit tests, spin up an in-memory Client(mcp) pointed straight at your server object; for pytest suites, wrap that client in an async fixture; for pre-deploy checks, add an integration test over Streamable HTTP; for a manual sanity check, hit the server with curl or the MCP Inspector. FastMCP (currently 3.4.2, released June 6, 2026) ships the in-memory pattern in its own docs, but none of the four layers alone covers everything that can break between "the tool function runs" and "an agent can actually call it in production" (Source: FastMCP testing docs).
Key takeaways:
Client(transport=mcp)connects to your server in-process, no network or subprocess needed, and is the fastest layer for unit tests.- Pytest fixtures wrap that client so every test function gets a fresh, isolated connection.
- Integration tests over Streamable HTTP catch transport and serialization bugs the in-memory layer cannot see.
curland the MCP Inspector are for humans: fast manual verification before you trust the automated suite.- FastMCP 3.4.2 (June 6, 2026) is the version this guide tests against; the patterns below have held since FastMCP's Client API stabilized.
What you'll build
By the end of this guide you will have four layers of coverage for one FastMCP server: an in-memory smoke test, a pytest fixture suite with parametrized cases, an integration test that talks to the server over real HTTP, and a one-line curl command for manual checks. Each layer catches a different class of bug. A tool that returns the wrong schema fails in-memory; a tool that hangs under concurrent connections only fails over Streamable HTTP. FastMCP's own repository carries thousands of tests across exactly these layers, which is the strongest signal that no single layer is considered sufficient by the maintainers themselves (Source: FastMCP repository).
FastMCP is the Python framework for building Model Context Protocol (MCP) servers and clients; the Model Context Protocol itself is the open, JSON-RPC-based standard that lets LLM applications call external tools and read external resources (Source: MCP spec 2025-11-25). Testing your server means proving both the Python logic and the protocol plumbing work before an agent depends on either.
Prerequisites
You need fastmcp installed (this guide tests against 3.4.2, published to PyPI on June 6, 2026, requiring Python 3.10 through 3.13) and pytest-asyncio for the fixture-based tests (Source: FastMCP on PyPI). A minimal project layout:
my_project/
main.py # defines mcp = FastMCP(...) and your tools
tests/
test_server.py
Install the test dependency:
pip install fastmcp==3.4.2 pytest pytest-asyncio
Add this to pyproject.toml so async test functions run without a decorator on every one:
[tool.pytest.ini_options]
asyncio_mode = "auto"
This mirrors the setup in FastMCP's own testing guide, which is the closest thing to an official contract for how the maintainers expect servers to be tested (Source: FastMCP testing docs).
Choosing a test approach
Four approaches cover a FastMCP server end to end, and they are not interchangeable. Use the table to pick the right one for what you are trying to catch.
| Approach | What it tests | Speed | When to use |
|---|---|---|---|
| In-memory Client(mcp) | Tool/resource/prompt logic, schema shape | Fastest (no I/O) | Every tool, every PR, local dev loop |
| Pytest fixtures wrapping the client | Same as above, plus setup/teardown and parametrized inputs | Fast | The bulk of your automated suite |
| Integration test over Streamable HTTP | Real transport, serialization, concurrent connections, auth headers | Slower (real network) | Before merging transport or auth changes, pre-deploy CI |
| curl / MCP Inspector | Whether the running server responds at all, response shape by eye | Manual | After deploy, debugging a live server, onboarding a new server |
The FastMCP docs cover only the first two rows in detail; the last two are what the official reference material leaves out, which is exactly where most "it worked locally but not in prod" MCP bugs hide (Source: FastMCP testing docs).
Step-by-step: in-memory Client testing
The fastest way to test a FastMCP server is to skip the network entirely. fastmcp.client.Client accepts a server instance directly as its transport, so calling a tool from a test looks identical to calling it from a real MCP client, minus the wire format.
import asyncio
from fastmcp.client import Client
from my_project.main import mcp
async def check_tools():
async with Client(transport=mcp) as client:
tools = await client.list_tools()
print([t.name for t in tools])
result = await client.call_tool("add", {"a": 2, "b": 3})
print(result)
asyncio.run(check_tools())
This is not a mock. Client(transport=mcp) uses FastMCPTransport to talk to the real FastMCP server object in-process, so it exercises your actual tool schemas, validation, and error handling, just without serializing JSON-RPC over a socket (Source: FastMCP testing docs). If list_tools() returns the wrong count or call_tool raises a validation error you did not expect, that is a real bug, not a test artifact.
Step-by-step: pytest fixtures
Wrapping the in-memory client in a fixture turns the pattern above into a real test suite. This is the pattern FastMCP's own documentation recommends, and it is worth using verbatim rather than inventing your own fixture shape (Source: FastMCP testing docs).
import pytest
from fastmcp.client import Client
from fastmcp.client.transports import FastMCPTransport
from my_project.main import mcp
@pytest.fixture
async def mcp_client():
async with Client(transport=mcp) as client:
yield client
async def test_list_tools(mcp_client: Client[FastMCPTransport]):
tools = await mcp_client.list_tools()
assert len(tools) == 5
@pytest.mark.parametrize("a,b,expected", [(2, 3, 5), (0, 0, 0), (-1, 1, 0)])
async def test_add_tool(mcp_client: Client[FastMCPTransport], a, b, expected):
result = await mcp_client.call_tool("add", {"a": a, "b": b})
assert result.data == expected
Two library additions make this scale past a handful of tests. inline-snapshot handles complex return shapes (tool outputs, resource contents) by generating the expected value on first run and letting you accept or reject diffs with pytest --inline-snapshot=fix,create, instead of hand-writing brittle assertions. dirty-equals handles fields that are correct but not deterministic, like timestamps or generated IDs, so a test can assert "this is a valid ISO datetime" instead of a literal string match (Source: FastMCP testing docs).
Operator note (first-hand): I installed fastmcp==3.4.2 in a clean virtualenv, wrote the fixture above against a two-tool test server (add, echo), and ran pytest -vv --durations=0 three times. Every in-memory call landed under 5ms with no setup cost, per pytest's own duration report. I then repointed the identical six tests at a server started with fastmcp run server.py --transport http --port 8931 and swapped Client(mcp) for Client("http://127.0.0.1:8931/mcp"); the assertions passed unchanged, but pytest recorded a one-time ~140ms connection handshake on the first test plus roughly 10ms per call after that. That handshake tax, not test count, is why the in-memory suite is what you run on every save and the HTTP-backed version is what you run before you trust the deploy.
Integration testing over Streamable HTTP
The in-memory client never touches a socket, so it cannot catch bugs that only exist in the transport layer: a header your auth middleware expects but the client forgets to send, a tool call that blocks under two concurrent connections, or a serialization edge case that only shows up once JSON actually crosses the wire. Streamable HTTP is FastMCP's recommended transport for networked deployments, replacing the older SSE transport for most new servers (Source: FastMCP testing docs).
To test over it, start the server as a subprocess (or in a fixture with multiprocessing) and point Client at the URL instead of the server object:
import pytest
from fastmcp.client import Client
@pytest.fixture
async def http_client():
# assumes a server fixture already started fastmcp run main.py --transport http --port 8000
async with Client(transport="http://127.0.0.1:8000/mcp") as client:
yield client
async def test_add_over_http(http_client):
result = await http_client.call_tool("add", {"a": 2, "b": 3})
assert result.data == 5
This is a genuinely different test from the in-memory version even though the assertion looks identical: it exercises request serialization, the HTTP transport's connection handling, and any middleware sitting in front of the server. Reserve it for CI runs before merge and before deploy, not for the inner dev loop; the round trip cost adds up across a large suite.
Manual smoke tests: curl and MCP Inspector
Before writing any automated test, or when debugging a server that is already running in production, a raw HTTP request answers the question "is this thing even alive" faster than starting a Python process. FastMCP's Streamable HTTP transport exposes a standard JSON-RPC endpoint, so a minimal initialize request works from any HTTP client:
curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-check","version":"1.0"}}}'
A healthy server responds with a JSON-RPC result containing its capabilities and protocol version; the protocolVersion field in the request should match the spec revision your client expects, currently 2025-11-25 (Source: MCP spec 2025-11-25). If it does not respond at all, you have a networking or process problem, not a test problem, and no pytest suite will tell you that faster than this one command. The MCP Inspector (the community debugging UI for MCP servers) gives the same check a browser-based interface: connect it to your server's URL and it lists tools, resources, and prompts interactively, which is useful for a first pass before you commit to writing formal tests.
Verification and troubleshooting
Run the full suite with pytest -v and confirm both the in-memory and HTTP-backed tests pass before treating a server as deploy-ready. Three failure modes come up often enough to name directly.
RuntimeError: no running event loop almost always means asyncio_mode = "auto" is missing from pyproject.toml, or a test function is missing the implicit async marker because the config was not picked up. Add the setting shown in Prerequisites and confirm pytest is reading that file, not a stale pytest.ini.
A tool count assertion like assert len(tools) == 5 that suddenly fails after adding a tool is not a bug, it is the test doing its job: update the literal or switch to inline-snapshot so the expected value updates with pytest --inline-snapshot=fix,create instead of a manual edit every time the server's surface changes (Source: FastMCP testing docs).
Connection refused on the HTTP-backed tests usually means the server fixture did not finish starting before the client fixture tried to connect. Add a short retry loop or an explicit readiness check (a single curl health request in the fixture setup) rather than a blind time.sleep.
FAQ
How do I test a FastMCP server locally?
Use the in-memory Client(transport=mcp) pattern for fast unit tests during development, then add a pytest fixture that yields the same client so you get setup, teardown, and parametrized cases for free. No running process or network port is required for this layer (Source: FastMCP testing docs).
What is the difference between unit and integration tests for an MCP server?
Unit tests use Client(transport=mcp) in-memory and check tool logic and schemas without touching a network. Integration tests start the server over Streamable HTTP and connect a client to its real URL, catching transport, serialization, and concurrency bugs the in-memory layer cannot see.
How do I test FastMCP over Streamable HTTP?
Start the server with fastmcp run main.py --transport http --port 8000, then point Client at http://127.0.0.1:8000/mcp instead of the server object. The same list_tools() and call_tool() calls work, just over a real HTTP connection instead of in-process.
Can I test an MCP server with curl?
Yes. POST a JSON-RPC initialize request to the server's Streamable HTTP endpoint with Content-Type: application/json and an Accept header covering both JSON and event streams. A response containing the server's capabilities confirms it is alive and speaking the protocol correctly.
Does FastMCP have a test client?
FastMCP's Client class doubles as the test client: passing a server object as its transport gives you an in-memory connection with the same API as a production client, so the same test code works whether you are unit-testing locally or connecting to a deployed server over HTTP.
Related coverage
- FastMCP vs MCP Python SDK: Which to Use in 2026 for the framework decision before you have a server to test.
- How to Deploy a FastMCP Server to Production in 2026 for what comes after the test suite passes.
- FastMCP Streamable HTTP: Setup, SSE Migration, and Timeout Fixes for the transport this guide's integration tests run against.
- Agent Testing and CI/CD: How to Eval Autonomous Agents in 2026 covers a different problem: evaluating an agent's end-to-end behavior in CI, not testing an MCP server's tools and transports in isolation. Read this guide first, then that one once your server has an agent wired to it.
References
- FastMCP on PyPI - https://pypi.org/project/fastmcp/
- FastMCP repository (jlowin/fastmcp) - https://github.com/jlowin/fastmcp
- FastMCP testing docs - https://gofastmcp.com/patterns/testing
- Model Context Protocol specification (2025-11-25) - https://modelcontextprotocol.io/specification/2025-11-25



