mcp-server-template
mcp-server-template
A Python MCP server template with the parts demos leave out: env-driven config, key auth with rotation, JSON logging with request IDs, and a golden-set eval harness. Fork it, replace the example tools, keep the shape.
Quick start
uv sync
uv run mcp-template # stdio server with seeded demo data
uv run pytest # unit + integration + e2e + eval contractsRegister it with Claude Desktop or Claude Code:
{
"mcpServers": {
"template": { "command": "uv", "args": ["run", "mcp-template"] }
}
}Why this exists
Most public MCP examples stop at the demo: one file, print statements, no tests. What separates that from something you can deploy is auth, observability, and evals, so those are the parts this template takes seriously.
Auth: a stdio server inherits the trust of whatever launched it, but the moment you expose streamable-http you need token verification and a rotation story. auth.py does constant-time key verification with a two-slot rotation window.
Observability: when a tool call fails inside an agent loop, you need to know which call, with what arguments, and how long it ran. Every call gets a request ID and a JSON log line on stderr. stdout belongs to the protocol.
Evals: tools drift. The harness in evals/ replays golden request/response contracts against the server in-process, so a behavior change fails CI before a client notices.
Layout
src/mcp_template/
├── server.py # FastMCP entrypoint; DuckDB lifecycle via lifespan
├── config.py # pydantic-settings, MCP_TEMPLATE_* env vars
├── log.py # JSON lines to stderr, request-id contextvar
├── auth.py # static key verifier, rotation window
└── tools/
├── registry.py # registration + per-call instrumentation
└── example_query.py # REPLACE-ME: guarded read-only DuckDB query tool
evals/
├── goldens/ # recorded request → expected response contracts
└── test_tool_contracts.py
tests/ # unit, in-process integration, stdio subprocess e2eConfig is all environment variables (MCP_TEMPLATE_*; the full list is in config.py). Unsafe combinations fail at startup: requiring auth with no keys configured is a ValueError, not a silent pass-through.
Adding a tool
Write the handler under
tools/with its unit tests.Register it in
register_all(tools/registry.py), wrapped in_instrument(...)so it logs like the rest.Record a golden contract in
evals/goldens/.Delete
example_query.pyonce you have real tools.
The example tool is worth reading before you delete it: single-statement SELECT/WITH validation before execution, parameter binding instead of string interpolation, a hard row cap with an explicit truncated flag, and database errors surfaced as tool errors rather than crashes. The guard is defense in depth; production should also run against a read-only connection or replica.
Tests
Four layers, all under uv run pytest:
Layer | Where | What it proves |
Unit |
| each module's behavior in isolation |
Integration |
| tools over the real protocol, in-process memory streams |
End-to-end |
| a spawned subprocess speaking stdio MCP |
Eval contracts |
| golden request/response stability across changes |
The eval layer is the one that pays for itself: change a tool and the contract diff tells you whether clients will notice.
Deploying
The Dockerfile builds a slim non-root image with locked dependencies. Inject MCP_TEMPLATE_AUTH_KEYS from your secret manager; never bake keys into the image. For streamable-http exposure, wire StaticKeyVerifier.verify into your HTTP layer or terminate auth at a reverse proxy.
License
MIT