Python's rich ecosystem makes it ideal for building MCP servers that integrate with data science tools, automation scripts, and backend services. This guide walks through creating a production-ready Python MCP server from scratch.
1. Prerequisites
Before we begin, ensure you have:
- Python 3.10 or higher: The MCP SDK uses modern Python features
- Basic understanding of async/await: MCP servers are asynchronous
- Familiarity with type hints: The SDK leverages Python's typing system
Verify Your Python Version
python --version # Should be 3.10 or higher
# or
python3 --version2. Project Setup
Start by creating a new project with a virtual environment:
# Create project directory
mkdir my-python-mcp-server
cd my-python-mcp-server
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install the MCP SDK
pip install mcpUsing uv (Recommended)
For faster dependency management, consider using uv—it's significantly faster than pip:
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create project with uv
uv init my-python-mcp-server
cd my-python-mcp-server
uv add mcpProject Structure
Organize your project like this:
my-python-mcp-server/
├── src/
│ └── my_mcp_server/
│ ├── __init__.py
│ ├── server.py
│ └── tools/
│ ├── __init__.py
│ └── calculator.py
├── tests/
│ └── test_server.py
├── pyproject.toml
└── README.md3. Basic Server Structure
Create src/my_mcp_server/server.py:
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
# Create the server instance
app = Server("my-python-server")
# Main entry point
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())Understanding the Structure
- Server instance: The
Serverclass is your main entry point. Give it a unique name. - stdio_server: Sets up communication over standard input/output—the most common transport.
- asyncio.run: Starts the async event loop and runs your server.
4. Defining Tools with Decorators
The Python SDK uses decorators to define tools. The docstring becomes the tool description, and type hints generate the input schema automatically.
from mcp.server import Server
from mcp.types import TextContent
app = Server("calculator-server")
@app.tool()
async def add(a: float, b: float) -> str:
"""
Add two numbers together.
Args:
a: The first number
b: The second number
Returns:
The sum of the two numbers
"""
result = a + b
return f"The sum of {a} and {b} is {result}"
@app.tool()
async def multiply(a: float, b: float) -> str:
"""
Multiply two numbers.
Args:
a: The first number
b: The second number
Returns:
The product of the two numbers
"""
result = a * b
return f"The product of {a} and {b} is {result}"
@app.tool()
async def calculate_percentage(value: float, percentage: float) -> str:
"""
Calculate a percentage of a value.
Args:
value: The base value
percentage: The percentage to calculate (e.g., 15 for 15%)
Returns:
The calculated percentage
"""
result = value * (percentage / 100)
return f"{percentage}% of {value} is {result}"Tool Definition Best Practices
- Use clear, descriptive function names
- Write detailed docstrings—the AI uses these to decide when to call your tool
- Use type hints for all parameters and return values
- Document each parameter in the Args section
- Return strings for simple responses, or use TextContent for rich content
Complex Input Types
For more complex inputs, use Pydantic models or dataclasses:
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class SearchQuery:
query: str
max_results: int = 10
include_archived: bool = False
@app.tool()
async def search_documents(params: SearchQuery) -> str:
"""
Search through documents.
Args:
params: Search parameters including query and options
"""
# Implementation here
return f"Found documents matching '{params.query}'"5. Adding Resources
Resources allow your server to expose data that the AI can read:
@app.resource("config://settings")
async def get_settings() -> str:
"""Current application settings"""
return """{
"debug": true,
"max_items": 100,
"api_version": "2.0"
}"""
@app.resource("stats://usage")
async def get_usage_stats() -> str:
"""Current usage statistics"""
import json
stats = {
"total_requests": 1234,
"active_users": 56,
"uptime_hours": 720
}
return json.dumps(stats, indent=2)Dynamic Resources with Templates
@app.resource("user://{user_id}/profile")
async def get_user_profile(user_id: str) -> str:
"""Get profile for a specific user"""
# Fetch user data
profile = await fetch_user(user_id)
return json.dumps(profile)6. Working with Context and State
For servers that need to maintain state or access shared resources:
from dataclasses import dataclass
from mcp.server import RequestContext
@dataclass
class AppContext:
database_url: str
cache: dict
api_client: Any
# Create context
context = AppContext(
database_url="postgresql://localhost/mydb",
cache={},
api_client=None
)
@app.tool()
async def query_database(
table: str,
ctx: RequestContext[AppContext]
) -> str:
"""
Query data from the database.
Args:
table: Name of the table to query
ctx: Request context with shared state
"""
db_url = ctx.state.database_url
# Check cache first
cache_key = f"table:{table}"
if cache_key in ctx.state.cache:
return ctx.state.cache[cache_key]
# Query database
result = await perform_query(db_url, table)
ctx.state.cache[cache_key] = result
return result7. Error Handling
Proper error handling is crucial for a good user experience:
from mcp.types import McpError, ErrorCode
@app.tool()
async def fetch_data(url: str) -> str:
"""
Fetch data from a URL.
Args:
url: The URL to fetch data from
"""
import aiohttp
# Validate input
if not url.startswith(("http://", "https://")):
raise McpError(
ErrorCode.InvalidParams,
f"Invalid URL: must start with http:// or https://"
)
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=30) as response:
if response.status != 200:
raise McpError(
ErrorCode.InternalError,
f"HTTP {response.status}: {response.reason}"
)
return await response.text()
except asyncio.TimeoutError:
raise McpError(
ErrorCode.InternalError,
f"Request timed out after 30 seconds"
)
except aiohttp.ClientError as e:
raise McpError(
ErrorCode.InternalError,
f"Network error: {str(e)}"
)8. Testing Your Server
Use the MCP Inspector to test your server:
npx @modelcontextprotocol/inspector python src/my_mcp_server/server.pyUnit Testing
# tests/test_server.py
import pytest
from my_mcp_server.server import add, multiply
@pytest.mark.asyncio
async def test_add():
result = await add(2, 3)
assert "5" in result
@pytest.mark.asyncio
async def test_multiply():
result = await multiply(4, 5)
assert "20" in result
@pytest.mark.asyncio
async def test_add_negative():
result = await add(-1, 1)
assert "0" in resultRunning with Claude Desktop
Add this to your Claude Desktop config:
{
"mcpServers": {
"my-python-server": {
"command": "python",
"args": ["/path/to/src/my_mcp_server/server.py"]
}
}
}9. Packaging for Distribution
To share your server, create a proper Python package:
pyproject.toml
[project]
name = "my-mcp-server"
version = "1.0.0"
description = "An MCP server for calculations"
requires-python = ">=3.10"
dependencies = [
"mcp>=1.0.0",
]
[project.scripts]
my-mcp-server = "my_mcp_server.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/my_mcp_server"]Publishing to PyPI
# Build the package
pip install build
python -m build
# Upload to PyPI
pip install twine
twine upload dist/*Users can then install and run your server with:
pip install my-mcp-server
my-mcp-server # Runs the serverOr use uvx for zero-install execution:
uvx my-mcp-serverNext Steps
- Explore the Python SDK repository for advanced examples
- Check out existing Python servers in our Server Directory
- Read about security best practices before deploying
Outdated Content Warning
This guide was last updated on January 9, 2025 (12 months ago).
The information presented here may be significantly outdated. Technologies, APIs, and best practices may have changed since this content was written.
We strive to keep our content current, but with rapidly evolving technologies, some details may no longer be accurate.
Last updated
January 9, 2025
377 days ago
This content may be outdated
This content may contain outdated information. Please verify details before use.