Skip to main content
Glama
michellepellon

mcp-server-template

README.md
# mcp-server-template

A Python [MCP](https://modelcontextprotocol.io) 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

```bash
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:

```json
{
  "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

TDQS

A3.8/5.0

Scored across 2 tools

Disambiguation5/5

run_query executes SQL statements while describe_tables returns schema metadata. The two tools have no functional overlap and are easy to distinguish.

Naming Consistency5/5

Both tools follow the same lowercase verb_noun pattern: run_query and describe_tables. Naming is predictable and consistent.

Tool Count3/5

Two tools is on the low end of reasonable for a focused read-only database server. The set is coherent but feels thin, leaving little flexibility for more advanced exploration.

Completeness4/5

The pair covers the core need of read-only SQL querying with schema discovery support. Minor gaps exist, such as no ability to inspect detailed table statistics or relationships, but agents can generally accomplish their main tasks.

Maintenance

ActivityInactive
ResponsivenessNo issues