MCP server scaffolding¶
The template can scaffold an MCP (Model
Context Protocol) server onto a cli or web_api project. Answering No
to use_recommended_integrations and Yes to include_mcp adds the mcp
SDK (with its CLI), generates an mcp_server.py with typed example tools,
and ships an in-process client test.
An MCP server is a long-running process — it does not fit the
"print and exit" CLI model. See
the layer model for why it is a module on
top of a normal cli / web_api project rather than its own project type.
This page has two audiences: developers building an MCP server (from the scaffold or their own) and users / operators adding MCP servers to a host. The security sections are split accordingly.
What gets generated¶
- The
mcp[cli]>=2.0,<3dependency (the SDK plus itsmcpcommand). - An
mcp_server.pyin your package exposing a module-levelserverobject (MCPServer) with: - two typed tools —
add(a: int, b: int)anddivide(...). The input schema comes from the type hints, no hand-written JSON Schema; - a
ToolErrorexample —divideby zero raises one, which the SDK turns into anis_error=Trueresult whose message the calling model can read (any other exception is sanitised); - two resources —
project://about(static) andgreeting://{name}(template: URI parameters map to function arguments); - one prompt —
review_code(a reusable message template the host surfaces as a slash command; the user picks it and fills incode). (The module lives inapp/mcp_server.pywhen the web_api layer is present, otherwise in<pkg>/mcp_server.py— one body shared via_shared/, so the two locations never drift.) - A console script
mcp-server-<name>so an MCP host can launch the server, and users can run it withuvxonce published. - An in-process client test
tests/test_mcp_server.pythat calls the tools, resources and prompt through the SDK'sClient— no subprocess, no port. Treat it as the worked example for calling this server from your own client code.
The MCP SDK v2 ships full type stubs, so mcp_server.py is covered by
the generated type checkers (no exclusions).
Running it¶
mcp_server.py exposes a module-level server object and a main() that
starts it. Replace <name> with your generated repo_name and <package>
with your generated package_name:
# stdio transport (the default): how an MCP host launches the server
uv run mcp-server-<name>
# Streamable HTTP (serve it on a port)
uv run python -m <package>.mcp_server --transport streamable-http
# Debug with the MCP Inspector (requires npx on PATH)
uv run mcp dev src/<package>/mcp_server.py
SSE is superseded by Streamable HTTP in the protocol; the scaffold only offers stdio and streamable-http.
Registering it with an MCP host¶
The generated project ships a .mcp.json pointing at the stdio command
(uv run mcp-server-<name>), so VS Code / Cursor and other hosts pick the
server up with no manual registration. Beyond that:
- Claude Code / Claude Desktop:
claude mcp add <name> -- uv run mcp-server-<name>(stdio), or the streamable-http URL after starting the server. - Published server: after releasing to PyPI (the
pypioption), users runuvx mcp-server-<name>— no install needed. Point.mcp.json(or the host config) at that command instead.
Consuming MCP servers (as a client)¶
Using an existing MCP server is out of scope for code generation: it is
either a host-configuration step (above) or a uvx mcp-server-... launch.
Writing Python code that calls an MCP server uses the same mcp dependency
and the same Client API that the generated test exercises — start from
tests/test_mcp_server.py.
Security for server developers¶
An MCP server is a remote-control surface for an LLM: the model picks arguments and your tools act on them. The scaffold keeps that surface small on purpose, and the SDK adds a few guarantees — the rest is yours to hold.
- Least privilege by default. The generated tools (
add,divide) touch nothing but their arguments. When you add real tools, give each one the narrowest possible scope: prefer read-only operations, validate and allowlist any path / URL / identifier the model hands you, and never let a tool accept a free-form shell command. A tool that can reach the network is an SSRF primitive — apply the same egress rules you would to any service. - Treat tool inputs as untrusted. Arguments come from a model that may be
following instructions found in files, emails or web pages it has read
(indirect prompt injection). Validate arguments against your domain rules
before acting, and make destructive operations explicit — raise
ToolErrorfor anything that looks out of bounds rather than guessing. - Error messages are part of the API.
ToolErrormessages reach the model verbatim; every other exception is sanitised toError executing tool <name>and only logged with its traceback. Never put secrets or internal paths in aToolErrormessage. - Exposing it over HTTP is a deployment decision. The scaffold's
streamable-http mode binds to
127.0.0.1for local debugging. Serving it to real clients is where MCP's own security settings kick in — see below.
Serving over HTTP / in production¶
The scaffold makes safe-by-default choices for the HTTP transport:
- The generated
main()binds to127.0.0.1unless you pass--host/--port(list them withmcp-server-<name> --help). - Binding to a non-local address requires the
MCP_ALLOWED_HOSTSenvironment variable (comma-separated host allowlist). The SDK only arms its DNS-rebinding protection while the server binds to localhost — binding anywhere else (e.g.0.0.0.0inside Docker) would otherwise accept every Host header.mcp_server.pyrefuses to start in that state instead of silently serving without protection.MCP_ALLOWED_ORIGINSis optional and passed through for browser clients. - A
GET /healthroute is registered for orchestrators. Like every custom route in the SDK it is unauthenticated and bypasses the host allowlist — it exists so a load balancer can probe the process before any MCP handshake; it exposes no tools or data..env.exampledocuments the variables.
Multi-worker deployments additionally need a shared RequestStateSecurity
key for multi-round-trip requests to survive reaching a different worker.
Both are documented in the SDK's
Deploy & scale page —
there is no MCP-specific Docker recipe, because the SDK deliberately leaves
process management to you (run the ASGI app under your usual uvicorn /
platform tooling). Authentication in front of the server is likewise yours:
the SDK supports OAuth, and a reverse proxy can add any other scheme. For a
Dockerized cli project with the docker option enabled, the generated
Dockerfile EXPOSEs port 8000 and the README / run-container how-to show the
docker run command (also available as the mcp-serve task).
The generated project already follows the template's security baseline
(pinned CI actions, dependency auditing, pip-audit, no secrets in the
repo).
Security for users and operators¶
Adding an MCP server to a host is not a no-op configuration step. A stdio server runs as a child process on your machine with your user's permissions — it is arbitrary code execution by another name — and a remote (streamable-http) server receives your prompts' tool calls. Whoever you connect to decides what your agent can see and do.
- Only connect servers you trust. Prefer servers from a source you can
audit: the project's own repository, a publisher you recognise, a PyPI
package you have checked. Treat a copy-pasted
uvx mcp-server-...command from an unfamiliar blog post or registry entry the same way you would acurl | shinstall script. Malicious and typosquatted MCP servers / agent skills are a documented, active threat (registry poisoning, credential-exfiltrating "skills", prompt-injected tool descriptions). - Read what the server can do before adding it. The server's
tools/listresult is its permission manifest: connect once in the MCP Inspector or a scratch client and look at the tool names, descriptions and argument schemas. A "filesystem" or "shell" tool is a wide-open grant; a server whose tools are narrow and specific is a small one. - Know where its instructions come from. Some servers fetch resources or load external content that can steer the model (indirect prompt injection). A server that reads web pages or emails is asking the model to act on untrusted text — keep it away from high-privilege tools.
- Grant it the least it needs. Configure per-server permissions in the host (folder access, allowed tools) when the host supports it. Never run an untrusted server inside a project that holds credentials, and prefer an isolated environment for servers you are evaluating.
- For remote servers, check the transport. A production streamable-http
server should sit behind TLS with real authentication — not an open
http://endpoint. If you self-host one, apply the deployment notes in the developer section above.
Where the token / config comes from¶
Like every long-running executable in this template, mcp_server.py reads
configuration from the environment (.env, loaded by direnv / the
compose stack) rather than from committed files. The generated .env.example
documents the variables the project understands — add yours there, never
commit the real .env.
Logging inside the server goes through the generated logging_setup.py, so
the LOG_FORMAT=json / structlog / loguru / ... behaviour is the same as
in the CLI.