Skip to content

Latest commit

 

History

History
161 lines (120 loc) · 6.69 KB

File metadata and controls

161 lines (120 loc) · 6.69 KB
layout default
title Chapter 2: Generated Project Structure and Conventions
nav_order 2
parent Create Python Server Tutorial

Chapter 2: Generated Project Structure and Conventions

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.

Learning Goals

  • 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

Generated Directory Layout

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]
Loading

File-by-File Breakdown

pyproject.toml

The generator modifies the uv init-generated pyproject.toml to add:

  • mcp>=1.0.0 as 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.

src/<package>/__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.

src/<package>/server.py

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.

README.md

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)

Naming Conventions

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]
Loading

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.

Template Rendering

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

Post-Generation File Ownership

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]
Loading

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.

Source References

Summary

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