A uv + FastMCP server template — a clean skeleton with the patterns, scripts, and glue you actually need, optimized for use with agentic coding tools (Claude Code, Cursor, Codex, Antigravity).
The demo domain is cake/baking (recipes, a simulated long-running bake, and a fake "Bakery Cloud" API for the auth demo). Replace the cake tools with your own; keep the structure.
Agents: start with AGENTS.md.
- uv packaging with the
uv_buildbackend. - Hybrid tool registration with modes (optional) — an
essentialssurface that's always on, plus anextendedtier a session opts into at runtime via theset_modetool (no restart, and scoped to that one client). Keeps the default tool list small to avoid polluting an agent's context. One demo tool ships in the extended tier; delete the axis entirely if your server has one surface. - Boot-safe runtime auth — the server never requires a key to start. Key-gated
tools resolve credentials per request / per session (multi-user safe), via
an
authenticatetool, an HTTP header, or env. Session keys live in FastMCP's own session state (ctx.set_state), not in a hand-rolled dict. - Real background tasks out of the box —
@mcp.tool(task=True)with FastMCP's in-memory backend (no Redis);FASTMCP_DOCKET_URLswitches to Redis for scale. - On-demand tool discovery (optional) —
--tool-search regex|bm25replaces the whole listing withsearch_tools+call_tool, for catalogs too big to put in an agent's context. - Structured I/O via Pydantic models + tool annotations.
- In-memory test harness (
Client(transport=server)) — fast, deterministic, no network. - Pre-wired client configs for Claude Code, Cursor, and VS Code.
- Optional Docker deployment.
uv sync
uv run pytest # 28 tests, all in-memory
uv run mcp-template stdio # run over stdio
uv run mcp-template http # run over HTTP (default :3011)
uv run mcp-template stdio --mode extended # add the opt-in tools
uv run mcp-template stdio --tool-search bm25 # discover tools by search
uv run fastmcp dev fastmcp.json # MCP InspectorThe server boots with no environment configured.
| Tool | Tier | Key? | Notes |
|---|---|---|---|
list_recipes |
essentials | no | read-only |
get_recipe |
essentials | no | read-only |
bake_cake |
essentials | no | real background task (task=True), streams progress |
authenticate |
always | — | unlocks gated tools for this session |
set_mode |
always | — | switches this session between the two surfaces |
scale_recipe |
extended | no | --mode extended |
order_custom_cake |
gated | yes | needs an API key (demo: cake_*) |
delivery_status |
gated | yes | needs an API key |
Gated tools are listed by default and refuse per call; CAKE_HIDE_GATED_UNTIL_AUTH=true
hides them from a session until it authenticates.
Plus a resource (resource://cakes/pantry) and a prompt (bake_a_cake).
CAKE_MODE (env) or --mode (CLI), default essentials, picks the starting
surface:
essentials— minimal, casual surface (low context cost).extended— adds the opt-in tools intools/extended.py.
Startup is not the last word. Any session can switch its own surface mid-
connection with the set_mode tool:
set_mode(mode="extended") # extended tools appear for THIS client
set_mode(mode="essentials") # ...and can be shed again to shrink the context
That matters most locally, where a stdio server's mode was fixed by whatever the
client's launch config said and changing it meant editing .mcp.json and
restarting. The extended tools are always registered; CAKE_MODE just decides
whether they start hidden. Switching calls the session-scoped
ctx.enable_components/disable_components, so other clients are unaffected and
the caller gets a tools/list_changed notification.
Exactly one tool (scale_recipe) sits in the extended tier — it is there to
show the shape, not to be a bucket to fill. Keep the split only if some of your
tools are genuinely too niche for the default surface; otherwise delete
tools/extended.py, its register_extended/register_mode_switch calls in
server.py, the mode setting, and the --mode CLI option.
CAKE_TOOL_SEARCH (env) or --tool-search (CLI), default off:
off— every registered tool is listed.regex— listing collapses tosearch_tools+call_tool; case-insensitive substring match over name, description and parameter docs. Deterministic and blunt: searchingbakealso matchesauthenticate, whose description mentions the Bakery Cloud.bm25— same shape, Okapi BM25 relevance ranking. Better for natural language.CAKE_TOOL_SEARCH_MAX_RESULTScaps the hits (default 5).
authenticate and set_mode stay pinned into the listing (ALWAYS_VISIBLE in
tool_search.py): a client that cannot see authenticate cannot unlock
anything, and set_mode is the only hint that an extended surface exists —
tools the mode axis hides are invisible to search as well.
Three things to know before enabling it:
- It is a FastMCP transform, not an MCP protocol feature — clients need to know nothing about it, they just see two tools.
- Unlisted tools stay callable by name, but a client cannot validate structured
output against a schema it never received: FastMCP's client degrades
result.datafrom a typed model to a plain dict. Usecall_toolfor typed results. - Only tools are replaced; resources and prompts list as usual. Search respects
auth and visibility, so it composes with
CAKE_HIDE_GATED_UNTIL_AUTH.
At this template's size it costs more than it saves — it earns its keep at dozens of tools.
The server never raises at startup for a missing key. Key-gated tools resolve a key per request, in this order:
X-Cake-Api-KeyHTTP header (multi-user safe)- per-session key set via the
authenticatetool CAKE_API_KEYenv (single-tenant / local default)
If none resolve, gated tools return a friendly message (no exception). A key set
via authenticate goes into FastMCP session state (ctx.set_state), which
namespaces it by ctx.session_id — so it never leaks between HTTP clients.
Two properties of that store to plan around:
- Entries expire after 24h; the session is then asked for a key again.
- The default backend is an in-process
MemoryStore. A multi-process HTTP deployment must share one —FastMCP(session_state_store=...)(e.g. Redis viapy-key-value) — or a worker will not see a key another worker stored.
By default the gated tools are listed to everyone and refuse politely per
call, so an agent can discover them before it has a key. Set
CAKE_HIDE_GATED_UNTIL_AUTH=true to hide them from a session until it
authenticates: startup disables them by tag, and authenticate calls the
session-scoped ctx.enable_components(tags={"bakery_cloud"}), which reveals
them to that client only (other sessions stay in the dark). The tradeoff — and
why it is off by default — is that an unauthenticated session cannot discover
the tools at all, and calling one by name fails with "Unknown tool" instead of
the friendly refusal.
Note the asymmetry: mcp.enable()/disable() are server-global (fine at
startup, never per user), while ctx.enable_components() is session-scoped
and is the only one safe to drive from a client's request.
Pre-wired configs are included:
- Claude Code →
.mcp.json - Cursor →
.cursor/mcp.json - VS Code →
.vscode/mcp.json
They launch uv run mcp-template stdio. For Codex (~/.codex/config.toml):
[mcp_servers.cake]
command = "uv"
args = ["run", "mcp-template", "stdio"]All CAKE_* env vars are optional (see .env.example and settings.py):
CAKE_API_KEY, CAKE_API_KEY_HEADER, CAKE_HIDE_GATED_UNTIL_AUTH,
CAKE_MODE, CAKE_TOOL_SEARCH, CAKE_TOOL_SEARCH_MAX_RESULTS,
CAKE_TRANSPORT, CAKE_HOST, CAKE_PORT, CAKE_LOG_LEVEL,
CAKE_OVEN_MAX_TEMP_C.
- Docker:
docker build -t cake-mcp . && docker run -p 3011:3011 cake-mcp(defaults to HTTP). For a publicly reachable HTTP deployment, opt into FastMCP's DNS-rebinding guard:FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=trueplusFASTMCP_HTTP_ALLOWED_HOSTS/FASTMCP_HTTP_ALLOWED_ORIGINS(off by default so local runs and reverse proxies keep working). - Declarative:
fastmcp.jsondrivesfastmcp run/fastmcp dev.
src/mcp_template/
server.py build_server(), CLI, graceful shutdown
settings.py pydantic-settings (CAKE_*), safe defaults
auth.py per-request key resolution (session state) + authenticate
tool_search.py optional: collapse the listing into search_tools/call_tool
models.py Pydantic tool I/O models
logging_setup.py stdlib logging -> stderr
tools/
recipes.py essentials (always)
extended.py extended tier + set_mode switch — optional pattern
bakery_cloud.py key-gated (tag: bakery_cloud)
data.py in-memory recipe fixtures
tests/ in-memory client tests
See the "Renaming the template" section in AGENTS.md.
MIT — see LICENSE.