PTENES
MODULE 5.1

📦 Packaging Your MCP Server

Turn your server into a professional Python package—installable, distributable, and ready to publish.

6
Topics
30
Minutes
Advanced
Level
Practical
Type
1

📦 Why Package

Having an MCP Server running as a script is great for development, but to distribute, version, and let others use your work, you need to turn it into a professional Python package. Packaging means creating a structure that lets you install with pip or uv, declare dependencies, define versions, and share reliably with the community.

💎 Core Concept

Package your MCP Server means turning it from a loose script into a Python project with a formal structure: metadata declared in the pyproject.toml, explicit dependencies, semantic versioning, and an entry point that lets you run the server with a single command in the terminal. You can install a package with pip install or uv pip install, distributed via PyPI or GitHub, and versioned with Git tags. The difference between a standalone script and a professional package is the difference between a prototype and a product.

❌ Avoid
  • •Distribute as a loose .py file by email
  • •Install dependencies manually on each machine
  • •Without versioning—it's impossible to know which version is running
  • •Installation instructions: "copy this file and hope for the best"
✅ Do
  • •Structure with pyproject.toml and src layout
  • •Declared and versioned dependencies
  • •Installation with one command: uv pip install .
  • •Clear, traceable versioning via semver
💡 Practical Tip

Think of packaging as putting your server in a professional toolbox with an instruction manual. Without the box, you have loose parts that only you know how to assemble. With the box, anyone can install and use it with a single command. Start with uv init to create the basic structure automatically.

2

📄 pyproject.toml

O pyproject.toml is the central file for any modern Python project. It replaces the old setup.py e setup.cfg with a clean declarative syntax standardized by PEP 621. This is where you define the package name, version, description, dependencies, and how the package should be built.

💎 Core Concept

A pyproject.toml typical for an MCP Server:

[project]
name = "meu-mcp-server"
version = "0.1.0"
description = "MCP Server para consulta de dados"
requires-python = ">=3.10"
dependencies = [
    "fastmcp>=0.1.0",
    "httpx>=0.25",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project.scripts]
meu-server = "meu_server.server:main"

The section [project] defines metadata, [build-system] defines how to build and [project.scripts] creates executable commands in the terminal.

💡 Practical Tip

Use hatchling as a build backend — it's modern, fast, and works perfectly with uv. If you prefer, setuptools also works, but hatchling is the recommended choice for new projects. Never write a setup.py — o pyproject.toml is the official standard since PEP 621.

📊 Data and Research

PEP 621 (2021) standardized Python project metadata in pyproject.toml. Since then, more than 85% of new packages published on PyPI adopted this format. The setup.py is considered legacy—it works, but isn’t recommended for new projects. Modern tools like uv, hatch e rye work natively with pyproject.toml.

3

📁 Directory Organization

A Python package’s directory structure follows conventions that make building, testing, and distribution easier. The pattern src layout is the recommended approach for avoiding import issues and ensuring that tests run against the installed package, not the local source code.

💎 Core Concept

Recommended structure for a packaged MCP Server:

meu-mcp-server/
├── pyproject.toml
├── README.md
├── src/
│   └── meu_server/
│       ├── __init__.py
│       └── server.py
└── tests/
    └── test_server.py

The directory src/ isolates the source code. The __init__.py marks the directory as a Python package. The server.py contains the MCP Server logic with the function main().

❌ Avoid
  • •Put server.py in the project root
  • •Forgetting __init__.py in the package
  • •Mixing tests with source code
  • •Use a flat layout without src/ for distributable packages
✅ Do
  • •Use a src layout for proper isolation
  • •Separate tests into their own directory
  • •Include README.md in the root
  • •Keep pyproject.toml at the root at all times
📋 Steps to Create the Structure
Step 1

Create the project's root directory and initialize it with uv init meu-mcp-server.

Step 2

Create src/meu_server/ with __init__.py and move the server code to server.py.

Step 3

Create directory tests/ and configure the pyproject.toml with metadata, dependencies, and entry points.

4

🔧 Entry Points

Entry points are the magic that lets you turn your MCP Server into a executable command in the terminal. Instead of running python -m meu_server.server, the user simply types meu-server and the package is run. This is configured in the [project.scripts] of the pyproject.toml.

💎 Core Concept

The entry point configuration in pyproject.toml:

[project.scripts]
meu-server = "meu_server.server:main"

This tells Python: "when someone types meu-server in the terminal, run the function main() from the file server.py inside the package meu_server." After installation, the command is available globally in the active virtual environment.

💡 Practical Tip

Make sure your function main() in server.py calls mcp.run(). This is the function that will be invoked when the user runs the command in the terminal. Always test with uv pip install -e . (editable mode) during development — this way, code changes are reflected immediately without reinstalling.

🚨 Alert

The command name (before the =) uses hyphens: meu-server. The module path (after the =) uses underscores: meu_server.server:main. Mixing the two is a common mistake that results in ModuleNotFoundError. Remember: hyphens in the terminal, underscores in Python.

5

📋 Dependencies and Versions

Declaring dependencies correctly is essential for your package to work on any machine. In pyproject.toml, you list all the libraries your server needs with version constraints that ensure compatibility without blocking updates. The uv lock generates a lockfile that pins exact versions for reproducibility.

💎 Core Concept

Dependencies are declared in the section [project] with semantic versioning (semver):

dependencies = [
    "fastmcp>=0.1.0",      # mínimo 0.1.0
    "httpx>=0.25,<1.0",    # entre 0.25 e 1.0
    "pydantic>=2.0",       # mínimo 2.0
]

Semver (semantic versioning) uses the format MAJOR.MINOR.PATCH: MAJOR changes when there are breaking changes, MINOR for new features, PATCH for bug fixes. Use >= for minimums and < for compatibility caps.

❌ Avoid
  • •Pin exact versions: fastmcp==0.1.0
  • •Don't declare a minimum version: httpx
  • •Forgetting dependencies that "are already installed"
  • •Don't run uv lock to generate a lockfile
✅ Do
  • •Use ranges: >=0.1.0,<1.0
  • •List ALL direct dependencies
  • •Run uv lock and commit the uv.lock
  • •Test in a clean environment before publishing
📊 Data and Research

According to PyPI analyses, 23% of problems of Python package installation are caused by poorly specified dependencies — incompatible versions, missing dependencies, or overly strict constraints. Using lockfiles (uv.lock) reduces reproducibility failures by more than 90%, ensuring that all developers and production environments use exactly the same versions.

6

🧪 Testing the Package

Before distributing your package, you need to verify that it works when installed — not only when run directly from the source code. This means building the package with uv build, install the file .whl generated and test that the entry point works correctly.

💎 Core Concept

The build process generates distributable files in the folder dist/:

# Construir o pacote
$ uv build
Successfully built meu_server-0.1.0-py3-none-any.whl

# Instalar o .whl gerado
$ uv pip install dist/meu_server-0.1.0-py3-none-any.whl

# Testar o entry point
$ meu-server
MCP Server rodando...

The file .whl (wheel) is the standard Python distribution format. It contains your code, metadata, and entry point information — everything pip/uv needs to install correctly.

📋 Package Testing Checklist
Step 1

Run uv build and verify that the .whl is generated without errors in the folder dist/.

Step 2

Create a clean virtual environment and install the .whl with uv pip install — simulates a real installation.

Step 3

Run the entry point command and verify that the server starts correctly and responds to MCP requests.

💡 Practical Tip

Always test in a clean virtual environment — not in the same environment where you developed it. This ensures all dependencies are correctly declared in the pyproject.toml. If something works in your dev environment but fails in a clean one, it means you forgot to declare a dependency.

📝 Module Summary

  • ✓ Packaging turns your server from a standalone script into a professional package—installable, distributable, and versioned.
  • ✓ pyproject.toml is the central file: metadata, dependencies, build system, and entry points all in one place.
  • ✓ The src layout isolates the source code and avoids import problems during testing and distribution.
  • ✓ Entry points create executable commands in the terminal—the user runs your server with a single command.
  • ✓ Dependencies should be declared with version ranges and locked with uv lock for reproducibility.
  • ✓ Always test the built package in a clean environment before distributing it.

Next Module: 5.2 — Publishing on GitHub