This guide outlines a practical design pattern to implement, test, and deploy an MCP (Model Context Protocol) server and a matching client using FastMCP.
Overview
This guide outlines a practical design pattern to implement, test, and deploy an MCP (Model Context Protocol) server and a matching client using FastMCP. It is framework- and cloud-agnostic with concrete examples where helpful. You will learn: - The recommended project layout - Core dependencies and environment setup - Server design patterns (tools, transports, serialization) - Client design patterns (connect, list, call) - Testing patterns (in-memory unit tests and HTTP integration tests) - Containerization and deployment options - server.py — FastMCP server, tools, and HTTP transport startup - client.py — Minimal FastMCP client example - test_mcp_inmemory.py — In‑memory, pytest-based unit tests that import the server directly - test_mcp_server.py — Simplified integration tests against the deployed HTTP server - Dockerfile — Container build for Cloud Run - pyproject.toml — Python project and dependency metadata - requirements.
README
Building an MCP Server and Client
This guide outlines a practical design pattern to implement, test, and deploy an MCP (Model Context Protocol) server and a matching client using FastMCP. It is framework- and cloud-agnostic with concrete examples where helpful.
You will learn:
- The recommended project layout
- Core dependencies and environment setup
- Server design patterns (tools, transports, serialization)
- Client design patterns (connect, list, call)
- Testing patterns (in-memory unit tests and HTTP integration tests)
- Containerization and deployment options
Recommended Project Structure
server.py— FastMCP server, tools, and HTTP transport startupclient.py— Minimal FastMCP client exampletest_mcp_inmemory.py— In‑memory, pytest-based unit tests that import the server directlytest_mcp_server.py— Simplified integration tests against the deployed HTTP serverDockerfile— Container build for Cloud Runpyproject.toml— Python project and dependency metadatarequirements.txt— Pinned runtime dependencies for Docker image
Server Design Pattern
Implement the server as a module that exports a single FastMCP instance and decorates Python functions as MCP tools. Keep business logic pure and testable, and return JSON-serializable results.
Core pattern:
from typing import Any, Dict, List
from fastmcp import FastMCP
mcp = FastMCP("Your MCP Server Name")
@mcp.tool()
def your_tool(arg1: str, limit: int = 10) -> List[Dict[str, Any]]:
"""Describe what this tool does in one sentence."""
# Validate inputs (FastMCP also validates based on type hints)
# Query/compute your results
return [{"ok": True, "arg1": arg1, "limit": limit}]
Transports:
- In-memory: ideal for testing via
Client(mcp) - HTTP (streamable): ideal for remote consumption by HTTP clients
- Other transports: depending on FastMCP support and your needs
HTTP startup:
# server.py
import asyncio, os
if __name__ == "__main__":
asyncio.run(
mcp.run_async(
transport="streamable-http",
host="0.0.0.0",
port=os.getenv("PORT", 8080),
)
)
Notes:
- Prefer explicit, typed function signatures. FastMCP generates JSON Schema automatically.
- Ensure all return values are JSON-serializable (dict, list, str, int, float, bool, None).
- If you expose HTTP, document the base path (e.g.,
/mcp/) and any auth requirements.
Client Design Pattern
Use the asynchronous context manager to connect, list tools, and call tools. When testing in-memory, pass the server instance directly; when testing remotely, pass the server URL.
Basic patterns:
from fastmcp import Client
# Remote HTTP client
async def main_remote():
async with Client("https://your-server.example.com/mcp/") as client:
await client.ping()
tools = await client.list_tools()
result = await client.call_tool("your_tool", {"arg1": "value", "limit": 5})
data = result.data
# In-memory client (great for unit tests)
async def main_inmemory(mcp):
async with Client(mcp) as client:
result = await client.call_tool("your_tool", {"arg1": "value"})
assert result.data
Tips:
- Treat
result.dataas the canonical output. Avoid assuming dict-like access for transport-wrapped items. - Prefer small, composable tools with clear inputs/outputs.
Local Development
- Python 3.11+ recommended (3.13 supported)
- Use a virtual environment
- Install dependencies via
uvorpip
Example setup:
python3 -m venv .venv
source .venv/bin/activate
uv pip install fastmcp fastapi uvicorn pytest pytest-asyncio # adjust as needed
# or: pip install ...
# Run server locally (HTTP on localhost:8080)
python server.py
# Quick smoke test (replace URL if needed)
python - <<'PY'
import asyncio
from fastmcp import Client
async def main():
async with Client("http://localhost:8080/mcp/") as c:
await c.ping()
print("OK: ping")
asyncio.run(main())
PY
Containerization and Deployment
Containerize your server to run anywhere. Common targets:
- Cloud Run (serverless containers)
- Kubernetes (GKE, EKS, AKS)
- Docker on VM or edge devices
Minimal Dockerfile (Python slim + requirements):
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "server.py"]
Example: deploy to Cloud Run (public access):
docker build -t gcr.io/PROJECT_ID/mcp-server:latest .
docker push gcr.io/PROJECT_ID/mcp-server:latest
gcloud run deploy mcp-server \
--image gcr.io/PROJECT_ID/mcp-server:latest \
--region us-central1 \
--platform managed \
--allow-unauthenticated
Auth notes:
- Private services require
roles/run.invokerand ID tokens on requests - Always document your service base path (e.g.,
/mcp/) and enforce trailing slashes to avoid redirects
Testing Strategy
Adopt a two-layer testing approach:
- In-memory unit tests (pytest)
- Import the
mcpinstance directly from your server module - Use
Client(mcp)to call tools without HTTP - Validate return shapes and edge cases deterministically
- Integration tests (unittest or pytest)
- Target the exposed HTTP endpoint
- Check liveness (
ping), tool discovery, and one or two happy-path tool calls - Avoid over-asserting internal shapes if the transport may wrap results
Result handling tips:
Client.call_tool()returnsCallToolResult, use.datafor the payload- Over HTTP, items may be wrapped (e.g.,
Root); assert invariants rather than direct key access unless you unwrap downstream - Prefer separate, narrower tools (e.g., a
get_details(name)tool) for deterministic verification
Common Pitfalls and Tips
- Trailing slash: If your HTTP transport mounts at
/mcp/, ensure clients use the trailing slash to avoid 30x redirects - Auth: Prefer unauthenticated endpoints for internal testing; add ID token or reverse proxy auth for production
- Serialization: Keep tool return values JSON-serializable and stable across versions
- Versioning: Introduce new tools or fields rather than breaking existing ones
- Observability: Add logging around tool entry/exit and validate inputs early
Extending the Server
- Add a new tool function with precise type hints
- Decorate with
@mcp.tool() - Keep logic pure and test via in-memory client
- Document inputs/outputs in the docstring so clients know what to pass/expect
License
This guide is provided as-is for demonstration and educational purposes.
Install
This server does not publish a one-line install command.
Open the repository installation guide New to MCP? Start with the mcpskill.org guide →