Skip to main content

Packaging & Distribution

Throughout this chapter, you've built powerful MCP servers: injecting Context, calling LLMs through clients, reporting progress, controlling file access, and handling errors gracefully. Now comes a critical step that separates toy projects from production-ready components: packaging your server so others can install and use it.

A packaged MCP server is fundamentally different from code in a git repository. With packaging, users run one command to install your entire server, including dependencies, and immediately use it in Claude Desktop or other MCP clients. No cloning repos, no installing dependencies manually, no configuration headaches.

This lesson teaches you the packaging patterns that turn your MCP server code into an installable Digital FTE component that integrates seamlessly into Claude and other workflows.

From Code to Package: The Transformation

When you have working MCP server code, the journey to distribution involves these steps:

  1. Define metadata (pyproject.toml) — What is this package? Who made it? What's required?
  2. Create entry points (command-line starters) — How does the server start after installation?
  3. Build the package (wheel creation) — Compress code and dependencies into installable form
  4. Test locally (installation verification) — Does it install cleanly? Does it work?
  5. Configure in clients (Claude Desktop) — Register the server so Claude can use it
  6. Distribute (PyPI, custom repos) — Make it available for others to install

Let's work through each step.

The pyproject.toml: Project Metadata and Dependencies

Your pyproject.toml file is the contract between your code and the outside world. It answers fundamental questions:

  • What is this project called?
  • What version is it?
  • What Python versions does it support?
  • What dependencies must be installed?
  • How do users run it after installation?

Minimal but Complete pyproject.toml

Here's the structure for an MCP server:

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

[project]
name = "my-mcp-server"
version = "0.1.0"
description = "MCP server for domain-specific tasks"
readme = "README.md"
requires-python = ">=3.11"
authors = [
{name = "Your Name", email = "you@example.com"},
]
keywords = ["mcp", "agent", "tools"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]

dependencies = [
"mcp>=1.6.0",
"httpx>=0.27.0",
"pydantic>=2.0.0",
]

[project.scripts]
my-mcp-server = "my_mcp_server:main"

[project.urls]
Homepage = "https://github.com/yourusername/my-mcp-server"
Repository = "https://github.com/yourusername/my-mcp-server"
Documentation = "https://github.com/yourusername/my-mcp-server/blob/main/README.md"

What each section does:

SectionPurposeExample
[build-system]Specifies how to build the packagehatchling is modern, lightweight builder
[project]Core metadata (name, version, description)Used by PyPI, pip, and installation tools
dependenciesRuntime requirements (what pip installs)MCP, httpx for HTTP requests, pydantic for validation
[project.scripts]Entry points (CLI commands created on install)my-mcp-server command becomes available after pip install
[project.urls]Project links (documentation, repository)Help users find source code and docs

Understanding the Entry Point Pattern

The [project.scripts] section is how your installed server becomes executable:

[project.scripts]
my-mcp-server = "my_mcp_server:main"

This line says:

  • When user runs my-mcp-server command, execute the main() function from module my_mcp_server
  • During installation, pip creates a CLI wrapper script that calls this function

Your Server's Entry Point Module

To make this work, you need a main() function in your package:

# my_mcp_server/__init__.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-server")

# ... define your tools, resources, prompts ...

def main():
"""Entry point for installed server."""
mcp.run()

if __name__ == "__main__":
main()

Why this structure?

  1. FastMCP global: Your mcp object is defined at module level, so all decorators (@mcp.tool, @mcp.resource) can attach to it
  2. main() function: Called by the entry point when user runs my-mcp-server command
  3. if name == "main": Allows testing by running the module directly: python -m my_mcp_server

Specifying Dependencies Precisely

Your dependencies list should include only what's essential for the server to run:

dependencies = [
"mcp>=1.6.0", # MCP framework (minimum version)
"httpx>=0.27.0", # For HTTP requests in your tools
"pydantic>=2.0.0", # Data validation (MCP requires this)
]

Dependency version specifications:

FormatMeaningUse Case
mcp>=1.6.0At least 1.6.0Framework is stable; new minor versions are safe
mcp~=1.6.01.6.x (not 1.7)Stricter; only patch version changes allowed
mcp==1.6.0Exactly 1.6.0Most restrictive; pinned version
mcp>=1.6.0,<2.01.6.0 to 1.xCommon pattern; avoid major version jumps

For MCP servers, use >=1.6.0 for framework dependencies. MCP follows semantic versioning, so minor versions are backward compatible.

Building Your Package

The uv build command creates a wheel (.whl file)—a compressed archive containing your code and a manifest.

Step 1: Prepare Project Structure

Ensure your project layout matches this pattern:

my-mcp-server/
├── my_mcp_server/ # Package directory (matches [project] name with underscores)
│ ├── __init__.py # Contains FastMCP instance and main()
│ ├── tools.py # Tool implementations
│ ├── resources.py # Resource implementations
│ └── prompts.py # Prompt definitions
├── pyproject.toml # Package metadata
├── README.md # Documentation
└── LICENSE # License (e.g., MIT)

Critical note: The directory name (my_mcp_server) must match your [project] name with hyphens converted to underscores.

Step 2: Build the Wheel

# Navigate to project directory
cd my-mcp-server

# Build the package
uv build

# Output:
# Building sdist (source distribution)...
# Building wheel...
# Successfully built my_mcp_server-0.1.0-py3-none-any.whl

The uv build command creates two artifacts:

  • *.tar.gz (source distribution) — for pip to install from source if needed
  • *.whl (wheel) — binary distribution, faster to install

Step 3: Inspect Package Contents

# List files in the wheel
unzip -l dist/my_mcp_server-0.1.0-py3-none-any.whl | head -30

# Output shows your code, metadata, entry points:
# my_mcp_server/__init__.py
# my_mcp_server/tools.py
# my_mcp_server-0.1.0.dist-info/entry_points.txt ← Entry point manifest
# my_mcp_server-0.1.0.dist-info/METADATA ← Project metadata from pyproject.toml

Local Testing: Installation and Verification

Before distributing to users, test your package locally:

Step 1: Install the Built Package

# Install the wheel (requires pip or uv)
uv pip install dist/my_mcp_server-0.1.0-py3-none-any.whl

# Or with pip:
pip install dist/my_mcp_server-0.1.0-py3-none-any.whl

After installation, the entry point is available as a command:

# Test that the command exists
which my-mcp-server
# Output: /path/to/venv/bin/my-mcp-server

# Check what it does
my-mcp-server --help
# Output: Usage: my-mcp-server [options]
# MCP server for domain-specific tasks

Step 2: Verify the Server Runs

# Start the server
my-mcp-server

# Expected output:
# Starting MCP server...
# Server running on stdio transport
# (Server waits for client connections; press Ctrl+C to stop)

The server should start without errors. If it crashes:

  1. Check imports: Verify all modules in my_mcp_server/ can be imported
  2. Check dependencies: Confirm all dependencies from pyproject.toml are installed
  3. Check main(): Ensure main() function is defined and calls mcp.run()

Step 3: Verify Entry Point Works

# List installed packages
pip show my-mcp-server

# Expected output:
# Name: my-mcp-server
# Version: 0.1.0
# Summary: MCP server for domain-specific tasks
# Location: /path/to/site-packages
# Requires: mcp, httpx, pydantic

# List available entry points
pip show --files my-mcp-server | grep entry_points

# Or check directly
cat /path/to/site-packages/my_mcp_server-0.1.0.dist-info/entry_points.txt

# Expected output:
# [console_scripts]
# my-mcp-server = my_mcp_server:main

Claude Desktop Configuration

After installing your MCP server, register it with Claude Desktop so Claude can discover and use its tools.

Step 1: Locate Claude Desktop Config

Claude Desktop stores server configurations in a JSON file:

macOS/Linux:

~/.config/Claude/claude_desktop_config.json

Windows:

%APPDATA%\Claude\claude_desktop_config.json

Step 2: Add Your Server to the Config

Edit the config file (or create it if it doesn't exist):

{
"mcpServers": {
"my-server": {
"command": "my-mcp-server"
}
}
}

Configuration breakdown:

{
"mcpServers": { // Top-level section for all MCP servers
"my-server": { // Server ID (internal name, can be anything)
"command": "my-mcp-server" // Exact command from [project.scripts]
}
}
}

Step 3: Restart Claude Desktop

Close and reopen Claude Desktop. After restart, Claude will:

  1. Start your MCP server via the my-mcp-server command
  2. Discover all tools, resources, and prompts your server defines
  3. Make them available in the Claude interface

Step 4: Verify Tools Appear

In Claude Desktop, check the bottom-left corner. You should see:

  • A "Tools" or "Integrations" menu
  • Your server name (e.g., "my-server")
  • List of available tools from your server

If tools don't appear:

  1. Restart Claude Desktop completely
  2. Check that my-mcp-server command is accessible (can you run it from terminal?)
  3. Review your server's tool definitions (@mcp.tool decorators)

Distribution Strategies

Once your package is built and tested locally, you have options for distribution:

Option 1: Share as Wheel File (Direct Installation)

Users can install directly from your wheel:

pip install my-mcp-server-0.1.0-py3-none-any.whl

Advantages:

  • No PyPI account needed
  • Users can install from GitHub releases, S3, etc.

Disadvantages:

  • Users need full filename (no pip install my-mcp-server without version)
  • No automatic updates

Option 2: Publish to PyPI (Official Python Package Index)

Upload your package to PyPI so users can install with a single command:

# User installation (after you publish):
pip install my-mcp-server

Advantages:

  • Standard Python installation experience
  • Automatic dependency resolution
  • pip install my-mcp-server without version numbers
  • PyPI hosts documentation

Disadvantages:

  • Requires PyPI account
  • Public name registration (first come, first served)
  • Version management responsibility

Publishing to PyPI involves:

  1. Creating a PyPI account (pypi.org)
  2. Building and signing your package
  3. Uploading with twine or uv publish

(Detailed PyPI publishing is beyond this lesson's scope; it's typically handled in a "Distribution & Publishing" chapter at the end of the course.)

Option 3: Private Distribution

For organizational use, distribute through:

  • GitHub releases: Users download .whl from your releases page
  • Private PyPI: Enterprise-grade package repository
  • Internal packages repo: Organization-hosted mirror

Try With AI

The patterns in this lesson are straightforward, but attention to detail matters. Work with AI to verify your packaging is correct.

Prompt 1: Validate Your pyproject.toml

What you're learning: How to identify potential configuration issues before they cause installation failures.

Ask Claude:

I've created a pyproject.toml for my MCP server. Please review it for completeness and correctness:

[paste your pyproject.toml]

Check:
1. Is [build-system] correctly specified?
2. Does [project] have all required fields (name, version, description, requires-python)?
3. Are dependencies correctly specified with version constraints?
4. Is the [project.scripts] entry point correctly formatted?
5. Are there any common mistakes or missing fields?

Expected result: Claude identifies any structural issues, missing metadata, or version constraint problems before you try to build.

Prompt 2: Debug Installation Failures

What you're learning: How to interpret installation errors and fix underlying packaging issues.

If installation fails, ask Claude:

My MCP server installation failed with this error:

[paste error message]

My pyproject.toml is:
[paste pyproject.toml]

My package structure is:
[paste directory listing]

My __init__.py contains:
[paste __init__.py code]

What's wrong, and how do I fix it?

Expected result: Claude traces from the error to the root cause (missing file, incorrect module path, dependency issue, etc.) and suggests specific fixes.

Prompt 3: Generate Complete pyproject.toml

What you're learning: How to leverage AI to generate correctly-formatted packaging boilerplate while you focus on your domain logic.

Ask Claude:

Generate a complete pyproject.toml for an MCP server with these specifications:

- Name: research-assistant
- Version: 0.2.1
- Description: MCP server that performs research tasks
- Python requirement: 3.11+
- Dependencies: mcp>=1.6.0, httpx>=0.27.0, pydantic>=2.0.0, requests>=2.31.0
- Entry point command: research-assistant
- Author: Your Name, email@example.com
- License: MIT
- GitHub: https://github.com/yourusername/research-assistant

Format it ready to copy into a new pyproject.toml file.

Expected result: Claude generates a complete, properly-formatted pyproject.toml that you can immediately use as your project's packaging configuration.


Reflect on Your Skill

You built an mcp-server skill in Lesson 0. Test and improve it based on what you learned.

Test Your Skill

Using my mcp-server skill, create a pyproject.toml configuration for packaging an MCP server.
Does my skill include guidance on entry points, dependency specifications, and build system configuration?

Identify Gaps

Ask yourself:

  • Did my skill include pyproject.toml structure ([project.scripts] entry points)?
  • Did it explain how to build with uv, test locally, and configure in Claude Desktop?

Improve Your Skill

If you found gaps:

My mcp-server skill is missing packaging and distribution patterns.
Update it to include pyproject.toml configuration, [project.scripts] entry point patterns, dependency version specifications, building with uv build, local installation testing, and Claude Desktop configuration.