Skip to main content
Glama
michellepellon

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 contracts

Register 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 e2e

Config 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

  1. Write the handler under tools/ with its unit tests.

  2. Register it in register_all (tools/registry.py), wrapped in _instrument(...) so it logs like the rest.

  3. Record a golden contract in evals/goldens/.

  4. Delete example_query.py once 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

tests/test_*.py

each module's behavior in isolation

Integration

tests/test_server_inprocess.py

tools over the real protocol, in-process memory streams

End-to-end

tests/test_stdio_e2e.py

a spawned subprocess speaking stdio MCP

Eval contracts

evals/

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