| layout | default |
|---|---|
| title | Chapter 2: Generated Project Structure and Conventions |
| nav_order | 2 |
| parent | Create Python Server Tutorial |
This chapter maps every file generated by create-mcp-server, explains the naming conventions the generator enforces, and shows how each piece supports maintainable server development.
- Navigate the scaffolded project structure at every path
- Map template files to runtime behavior and MCP primitive registration
- Understand naming and package conventions used by the generator
- Keep customization changes isolated from generated boilerplate
my-notes-server/
├── README.md # Rendered from README.md.jinja2
├── pyproject.toml # uv project config + mcp dependency
├── uv.lock # Locked dependency tree
└── src/
└── my_notes_server/ # Package dir: project name, hyphens → underscores
├── __init__.py # Rendered from __init__.py.jinja2 (entry point)
└── server.py # Rendered from server.py.jinja2 (MCP handlers)
graph TD
ROOT[my-notes-server/]
ROOT --> README[README.md\nUsage + integration guide]
ROOT --> PYPROJECT[pyproject.toml\nPackaging + dependencies]
ROOT --> LOCK[uv.lock\nReproducible dependency tree]
ROOT --> SRC[src/]
SRC --> PKG[my_notes_server/\nPython package]
PKG --> INIT[__init__.py\nMain entry point\ncalls asyncio.run on server.main]
PKG --> SERVER[server.py\nMCP handlers:\ntools · resources · prompts]
The generator modifies the uv init-generated pyproject.toml to add:
mcp>=1.0.0as a runtime dependency- A
[project.scripts]entry pointing the binary name at<package>:main
[project]
name = "my-notes-server"
version = "0.1.0"
description = "A simple MCP server for managing notes"
requires-python = ">=3.10"
dependencies = ["mcp>=1.0.0"]
[project.scripts]
my-notes-server = "my_notes_server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"The [project.scripts] entry is what makes uv run my-notes-server and uvx my-notes-server work — it maps the binary name to the main() function in __init__.py.
Rendered from __init__.py.jinja2, this file provides the synchronous entry point:
from . import server
import asyncio
def main():
asyncio.run(server.main())The generator uses the first_binary property of the PyProject class to ensure the function name matches the scripts entry. This indirection keeps server.py purely async and testable in isolation.
The core implementation file, rendered from server.py.jinja2. This is where all MCP primitive handlers live. It is the primary file developers modify after scaffolding.
Rendered from README.md.jinja2, the README contains:
- Installation instructions (
uv sync --dev --all-extras) - Claude Desktop configuration snippet (pre-filled with the project name)
- Development command (
npx @modelcontextprotocol/inspector ...) - Build and publish instructions (
uv build,uv publish)
The generator enforces Python package naming from the project name string:
| Input | Converted to |
|---|---|
my-notes-server |
my_notes_server (package dir, hyphens → underscores) |
my-notes-server |
my-notes-server (binary name in scripts, preserved) |
my-notes-server |
"my-notes-server" (server name in Server("...") call) |
flowchart LR
INPUT[project name: my-notes-server]
INPUT -->|hyphens to underscores| PKG[package dir:\nsrc/my_notes_server/]
INPUT -->|preserved| BIN[binary:\nuv run my-notes-server]
INPUT -->|preserved| SNAME[Server name:\nServer\nmy-notes-server\n]
Important: The Jinja2 templates reference {{server_name}} for display name and {{binary_name}} for the entry point. These are substituted by copy_template() during generation and are not present in the final generated files.
The copy_template() function in __init__.py uses Jinja2 to render all three template files:
template_vars = {
"binary_name": bin_name, # from pyproject.toml scripts
"server_name": name, # project name as entered
"server_version": version, # "0.1.0" default
"server_description": description,
"server_directory": str(path.resolve()),
}Files rendered:
| Template | Output Location | Key Variables Used |
|---|---|---|
__init__.py.jinja2 |
src/<pkg>/__init__.py |
binary_name |
server.py.jinja2 |
src/<pkg>/server.py |
server_name, server_version |
README.md.jinja2 |
README.md |
server_name, binary_name, server_directory |
graph LR
GENERATED[Generated Files]
GENERATED --> STABLE[Stable — rarely change:\npyproject.toml · uv.lock\n__init__.py]
GENERATED --> MODIFY[Primary modification target:\nserver.py]
GENERATED --> DOCS[Keep updated:\nREADME.md]
The convention is: treat __init__.py as scaffolding boilerplate (don't modify), and concentrate all MCP logic in server.py. When customizing heavily, split server.py into multiple modules and import them — but keep the server.py file as the handler registration hub.
- Template Server (
server.py.jinja2) - Template Entry Point (
__init__.py.jinja2) - Template README (
README.md.jinja2) - Generator Logic (
__init__.py)
The generator produces a five-file project: pyproject.toml, uv.lock, README.md, __init__.py (entry point shim), and server.py (handler implementation). Naming follows Python package conventions (hyphens → underscores for directory, preserved for binary). All customization should focus on server.py; treat the rest as scaffolding until deliberate changes are needed.
Next: Chapter 3: Template Server Architecture: Resources, Prompts, and Tools