A production-ready template for building Python MCP (Model Context Protocol) servers using FastMCP.
⚠️ This is a template — it does not do anything useful until you edit it. Out of the box it ships a singlehellotool that echoes a greeting. You need to add your own tools, and those tools will almost always need a client (HTTP, SDK, DB driver, etc.) to talk to whatever backend they wrap. See Implementing New Tools.This template is designed to run behind ToolHive. Auth is delegated to ToolHive — see Authentication.
- FastMCP server with an example tool implementation
- Token passthrough — Bearer tokens from MCP clients are available to tools via context. Requests without a Bearer token are rejected with 401 by default (
REQUIRE_BEARER_TOKEN=true); set tofalsefor local development - Pydantic for data validation and type safety
- Task automation via Taskfile for common operations
- Testing infrastructure with pytest and pytest-asyncio
- Code quality tools: ruff (linting/formatting), ty (type checking)
- Security scanning: bandit, pip-audit, cyclonedx-bom (SBOM), Grype (container + filesystem)
- Hardened containers built on Docker Hardened Images (DHI) — multi-arch (amd64/arm64)
- Release pipeline (shipped as stubs): Cosign signing, SLSA provenance attestation, GitHub Releases with auto-generated notes
- GitHub Actions for CI/CD, code quality, and automated builds
# Install dependencies
task install
# Or without task:
uv sync --dev --all-packages --group security
# Copy .env (needed for `task compose` even if empty)
cp .env.example .env# Run locally
task run
# Or with Docker Compose (requires DHI authentication, see below)
task composeThe server runs on http://0.0.0.0:8100 by default.
The Dockerfile and CI image builds use Docker Hardened Images (DHI). DHI is free (Community tier, Apache 2.0) but pulls from dhi.io require authentication with a Docker Hub account:
# A free Docker Hub account works — no paid subscription required
docker login dhi.ioIf you use this template for your own repo, also see When using this template below for the GitHub secrets CI needs.
Tools are implemented in src/mcp_template_py/api/tools.py as methods on the Tools class, and registered in src/mcp_template_py/api/mcp_builder.py:
1. Define the response model in src/mcp_template_py/api/models.py:
from pydantic import BaseModel, Field
class HelloResponse(BaseModel):
result: str = Field(..., description="The greeting message.")2. Implement the tool in src/mcp_template_py/api/tools.py with flat arguments:
class Tools:
async def hello(self, name: str) -> HelloResponse:
"""Say hello to the user."""
return HelloResponse(result=f"Hello, {name}!")3. Register the tool in src/mcp_template_py/api/mcp_builder.py:
tools = Tools()
mcp.add_tool(tools.hello)Key points:
- Tools are async methods on the
Toolsclass - Arguments are flat parameters (e.g.
name: str, age: int), not a single wrapper Pydantic model. MCP clients see each argument as its own field, not a nestedrequestobject - Return a Pydantic model so the tool has an explicit, typed output schema
- Docstrings become tool descriptions for MCP clients; use
Annotated[..., Field(description=...)]on a parameter if you want per-argument descriptions in the schema - Tools are registered via
mcp.add_tool()in the MCP builder - Use
get_bearer_token()frommcp_template_py.authto access the client's Bearer token
This template does not implement its own auth server. It expects to run behind ToolHive, which handles identity, token validation, and policy. To get auth working, configure the auth server in ToolHive — see the ToolHive authentication docs.
What this server does is passthrough: it takes the Authorization: Bearer <token>
header that ToolHive forwards and makes the token available to your tools so they can
forward it to whatever upstream API they call.
from mcp_template_py.auth import get_bearer_token
token = get_bearer_token() # token forwarded by ToolHiveFor local development without ToolHive, set REQUIRE_BEARER_TOKEN=false to skip the
401-on-missing-token check.
| Command | Description |
|---|---|
task install |
Install all dependencies (dev + security) |
task run |
Run the MCP server locally |
task compose |
Start server with Docker Compose |
task lint |
Run ruff linter |
task format |
Format code and fix lint issues |
task typecheck |
Run ty type checker |
task test |
Run pytest tests |
task security |
Run bandit + pip-audit |
task sbom |
Generate a CycloneDX SBOM |
task check |
Run all checks (lint, typecheck, test, security) |
src/mcp_template_py/
├── __main__.py # Server entry point
├── settings.py # Configuration and environment variables
├── configure_logging.py # Logging configuration
├── api/
│ ├── app_builder.py # Starlette application factory
│ ├── mcp_builder.py # FastMCP server builder
│ ├── tools.py # MCP tool implementations
│ └── models.py # Pydantic models for tools
└── auth/
└── mcp_auth_middleware.py # Token passthrough middleware
tests/
├── unit/ # Unit tests with mocked dependencies
│ ├── test_settings.py # Settings validation tests
│ └── test_token_passthrough.py # Middleware tests
└── integration/ # Integration tests (requires running server)
└── test_mcp.py # MCP tool tests
Configuration is managed through environment variables. Copy .env.example to .env and configure as needed.
| Variable | Default | Description |
|---|---|---|
DEBUG |
false |
Enable debug logging |
MCP_HOST |
0.0.0.0 |
Host for the MCP server to listen on |
MCP_PORT |
8100 |
Port for the MCP server to listen on |
SERVER_URL |
http://localhost:8100 |
Base URL of the server |
REQUIRE_BEARER_TOKEN |
true |
Reject requests without a Bearer token |
Integration tests (tests/integration/) require the MCP server to be running (task run or task compose) and will be skipped if unreachable.
# Run all tests
task test
# Run with coverage
uv run pytest --cov=src/mcp_template_pyWhether you created a new repo via "Use this template" or actually forked this one, CI needs a few GitHub Actions secrets to work in your copy:
Required for image builds and security scans (image-build.yml, security.yml):
DOCKERHUB_USERNAME— your Docker Hub usernameDOCKERHUB_TOKEN— a Docker Hub access token with public-read scope
Required when you enable the release pipeline (see docs/release-playbook.md):
MCP_RELEASE_WORKFLOW_APP_ID— numeric App ID of a GitHub App installed on your repo with Contents: Read/WriteMCP_RELEASE_WORKFLOW_APP_KEY— the App's private key (full.pemcontents)
The release workflows (release.yml, create-release.yml, patch-release.yml) ship stubbed so the template itself does not publish artifacts. To enable them in your copy, follow the unstub steps in docs/release-playbook.md.
See CONTRIBUTING.md for development setup, PR conventions, and the DCO sign-off requirement.
To report a vulnerability, please use the GitHub Security Advisory flow described in SECURITY.md — do not file a public issue.