All skills
Skillintermediate

Python MCP Server Implementation

<overview> Python implementation using the mcp package provides clean async/await patterns, decorator-based APIs, and strong type hints. This guide covers Python-specific features and best practices. </overview>

Claude Code Knowledge Pack7/10/2026

Overview

Python MCP Server Implementation

<overview> Python implementation using the mcp package provides clean async/await patterns, decorator-based APIs, and strong type hints. This guide covers Python-specific features and best practices. </overview>

Project Setup

<dependencies> ```toml # pyproject.toml [project] name = "my-mcp-server" version = "1.0.0" dependencies = [ "mcp>=0.1.0", "pydantic>=2.0.0", ]

[project.optional-dependencies] dev = [ "pytest>=7.0.0", "pytest-asyncio>=0.21.0", "mypy>=1.0.0", ]

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

Or using setup.py/requirements.txt:

mcp>=0.1.0

pydantic>=2.0.0

</dependencies>

<directory_structure>

my_mcp_server/ ├── init.py ├── server.py # Main server implementation ├── tools/ # Tool implementations │ ├── init.py │ ├── calculator.py │ └── api_client.py ├── resources/ # Resource handlers │ ├── init.py │ └── file_system.py └── config.py # Configuration

</directory_structure>

## Server Structure

<full_example>
```python
"""MCP Server implementation."""

from typing import Any

from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
    Tool,
    TextContent,
    ImageContent,
    EmbeddedResource,
    Resource,
    Prompt,
    PromptMessage,
    GetPromptResult,
)
from pydantic import BaseModel, Field

# Server instance
app = Server("my-mcp-server")

# Type-safe argument models using Pydantic
class AddNumbersArgs(BaseModel):
    """Arguments for add_numbers tool."""
    a: float = Field(description="First number")
    b: float = Field(description="Second number")

class SearchArgs(BaseModel):
    """Arguments for search tool."""
    query: str = Field(min_length=1, max_length=500, description="Search query")
    limit: int = Field(default=10, ge=1, le=100, description="Maximum results")
    filters: list[str] | None = Field(default=None, description="Optional filters")

# Tool handlers
@app.list_tools()
async def list_tools() -> list[Tool]:
    """List available tools."""
    return [
        Tool(
            name="add_numbers",
            description="Add two numbers together",
            inputSchema=AddNumbersArgs.model_json_schema(),
        ),
        Tool(
            name="search",
            description="Search for items",
            inputSchema=SearchArgs.model_json_schema(),
        ),
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
    """Handle tool calls."""
    if name == "add_numbers":
        args = AddNumbersArgs(**arguments)
        result = args.a + args.b
        return [TextContent(
            type="text",
            text=f"{args.a} + {args.b} = {result}"
        )]

    elif name == "search":
        args = SearchArgs(**arguments)
        results = await perform_search(args.query, args.limit, args.filters)
        return [TextContent(
            type="text",
            text=f"Found {len(results)} results for '{args.query}'"
        )]

    raise ValueError(f"Unknown tool: {name}")

# Resource handlers
@app.list_resources()
async def list_resources() -> list[Resource]:
    """List available resources."""
    return [
        Resource(
            uri="config://settings",
            name="Server Configuration",
            description="Current server settings",
            mimeType="application/json",
        ),
        Resource(
            uri="file:///{path}",
            name="File System",
            description="Read files from filesystem",
            mimeType="text/plain",
        ),
    ]

@app.read_resource()
async def read_resource(uri: str) -> str:
    """Read a resource by URI."""
    if uri == "config://settings":

        config = load_config()
        return json.dumps(config.__dict__, indent=2)

    elif uri.startswith("file:///"):
        path = uri[8:]  # Remove "file:///"
        async with aiofiles.open(path, "r") as f:
            return await f.read()

    raise ValueError(f"Unknown resource: {uri}")

# Prompt handlers
@app.list_prompts()
async def list_prompts() -> list[Prompt]:
    """List available prompts."""
    return [
        Prompt(
            name="code_review",
            description="Review code for best practices",
            arguments=[
                {"name": "language", "description": "Programming language", "required": True},
                {"name": "code", "description": "Code to review", "required": True},
            ],
        ),
    ]

@app.get_prompt()
async def get_prompt(name: str, arguments: dict[str, str] | None) -> GetPromptResult:
    """Get a prompt by name."""
    if name == "code_review":
        language = arguments.get("language", "unknown")
        code = arguments.get("code", "")

        return GetPromptResult(
            description=f"Code review for {language}",
            messages=[
                PromptMessage(
                    role="user",
                    content=TextContent(
                        type="text",
                        text=f"Review this {language} code for best practices:\
\
{code}"
                    ),
                ),
            ],
        )

    raise ValueError(f"Unknown prompt: {name}")

# Helper functions
async def perform_search(query: str, limit: int, filters: list[str] | None) -> list[dict]:
    """Perform search operation."""
    # Implementation here
    return []

def load_config():
    """Load server configuration."""
    from .config import Config
    return Config()

# Main entry point
async def main():
    """Run the MCP server."""
    async with stdio_server() as (read_stream, write_stream):
        # Log to stderr (stdout is for MCP protocol)
        print("my-mcp-server starting...", file=sys.stderr)

        await app.run(
            read_stream,
            write_stream,
            app.create_initialization_options()
        )

def run():
    """Synchronous entry point for CLI."""
    asyncio.run(main())

if __name__ == "__main__":
    run()

</full_example>

Type-Safe Patterns

<pydantic_models> Using Pydantic for Validation:

from pydantic import BaseModel, Field, field_validator, model_validator
from typing import Annotated

class SearchArgs(BaseModel):
    """Search arguments with validation."""
    query: Annotated[str, Field(min_length=1, max_length=500)]
    limit: Annotated[int, Field(ge=1, le=100)] = 10
    sort_by: str | None = None
    ascending: bool = True

    @field_validator('query')
    @classmethod
    def validate_query(cls, v: str) -> str:
        """Validate search query."""
        if v.strip() != v:
            raise ValueError("Query cannot have leading/trailing whitespace")
        return v

    @model_validator(mode='after')
    def validate_sort(self) -> 'SearchArgs':
        """Validate sort parameters."""
        if self.sort_by and self.sort_by not in ['date', 'relevance', 'title']:
            raise ValueError(f"Invalid sort_by: {self.sort_by}")
        return self

# Use in tool handler
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
    if name == "search":
        try:
            args = SearchArgs(**arguments)
            results = await perform_search(args)
            return [TextContent(type="text", text=str(results))]
        except ValidationError as e:
            # Return validation errors to Claude
            return [TextContent(
                type="text",
                text=f"Invalid arguments: {e}"
            )]

    raise ValueError(f"Unknown tool: {name}")

</pydantic_models>

<async_patterns> Async/Await Best Practices:


from typing import Any

# Concurrent operations
async def fetch_multiple_sources(queries: list[str]) -> list[dict]:
    """Fetch from multiple sources concurrently."""
    tasks = [fetch_source(query) for query in queries]
    results = await asyncio.gather(*tasks, return_exceptions=True)

    # Filter out errors
    return [r for r in results if not isinstance(r, Exception)]

async def fetch_source(query: str) -> dict:
    """Fetch from a single source."""
    async with aiohttp.ClientSession() as session:
        async with session.get(f"https://api.example.com/search?q={query}") as response:
            return await response.json()

# Timeout handling
async def tool_with_timeout(args: dict) -> TextContent:
    """Tool with timeout protection."""
    try:
        result = await asyncio.wait_for(
            slow_operation(args),
            timeout=30.0  # 30 second timeout
        )
        return TextContent(type="text", text=result)
    except asyncio.TimeoutError:
        return TextContent(
            type="text",
            text="Operation timed out after 30 seconds"
        )

# Background tasks
class ServerState:
    """Maintain server state."""
    def __init__(self):
        self.cache: dict[str, Any] = {}
        self._cleanup_task: asyncio.Task | None = None

    async def start_cleanup(self):
        """Start background cleanup task."""
        self._cleanup_task = asyncio.create_task(self._cleanup_loop())

    async def _cleanup_loop(self):
        """Periodically clean cache."""
        while True:
            await asyncio.sleep(300)  # Every 5 minutes
            self.cache.clear()
            print("Cache cleaned", file=sys.stderr)

    async def stop(self):
        """Stop background tasks."""
        if self._cleanup_task:
            self._cleanup_task.cancel()
            try:
                await self._cleanup_task
            except asyncio.CancelledError:
                pass

state = ServerState()

async def main():
    """Run server with state management."""
    await state.start_cleanup()

    try:
        async with stdio_server() as (read_stream, write_stream):
            await app.run(
                read_stream,
                write_stream,
                app.create_initialization_options()
            )
    finally:
        await state.stop()

</async_patterns>

Error Handling

<error_patterns>


from typing import Any
from mcp.types import TextContent

class ToolError(Exception):
    """Base exception for tool errors."""
    pass

class InvalidArgumentError(ToolError):
    """Invalid tool arguments."""
    pass

class ExternalAPIError(ToolError):
    """External API call failed."""
    pass

@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
    """Handle tool calls with comprehensive error handling."""
    try:
        # Validate tool exists
        if name not in AVAILABLE_TOOLS:
            raise ToolError(f"Unknown tool: {name}")

        # Execute tool
        result = await execute_tool(name, arguments)
        return [result]

    except InvalidArgumentError as e:
        # Client error - return helpful message
        print(f"Invalid arguments for {name}: {e}", file=sys.stderr)
        return [TextContent(
            type="text",
            text=f"Invalid arguments: {e}\
\
Please check the tool's input schema."
        )]

    except ExternalAPIError as e:
        # Upstream error - inform user
        print(f"External API error in {name}: {e}", file=sys.stderr)
        return [TextContent(
            type="text",
            text=f"External service error: {e}\
\
Please try again later."
        )]

    except Exception as e:
        # Unexpected error - log and return generic message
        print(f"Unexpected error in {name}:", file=sys.stderr)
        traceback.print_exc(file=sys.stderr)
        return [TextContent(
            type="text",
            text=f"An unexpected error occurred. Please contact support."
        )]

async def execute_tool(name: str, arguments: dict[str, Any]) -> TextContent:
    """Execute a tool with proper error handling."""
    if name == "api_call":
        try:
            args = APICallArgs(**arguments)
        except Exception as e:
            raise InvalidArgumentError(str(e)) from e

        try:
            response = await make_api_request(args)
            return TextContent(type="text", text=response)
        except aiohttp.ClientError as e:
            raise ExternalAPIError(f"API request failed: {e}") from e

    raise ToolError(f"Unknown tool: {name}")

</error_patterns>

Configuration

<env_config>

# config.py

from dataclasses import dataclass
from pathlib import Path

@dataclass
class Config:
    """Server configuration."""
    api_key: str
    api_endpoint: str
    max_retries: int
    debug: bool
    cache_dir: Path

    @classmethod
    def from_env(cls) -> 'Config':
        """Load configuration from environment variables."""
        api_key = os.getenv("API_KEY")
        if not api_key:
            raise ValueError("API_KEY environment variable is required")

        return cls(
            api_key=api_key,
            api_endpoint=os.getenv("API_ENDPOINT", "https://api.example.com"),
            max_retries=int(os.getenv("MAX_RETRIES", "3")),
            debug=os.getenv("DEBUG", "").lower() == "true",
            cache_dir=Path(os.getenv("CACHE_DIR", "~/.cache/my-mcp-server")).expanduser(),
        )

    def __post_init__(self):
        """Validate and prepare configuration."""
        # Ensure cache directory exists
        self.cache_dir.mkdir(parents=True, exist_ok=True)

        # Log configuration (without secrets)
        if self.debug:
            print(f"Config: endpoint={self.api_endpoint}, retries={self.max_retries}",
                  file=sys.stderr)

# Load config globally
config = Config.from_env()

# Use in tools
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
    if name == "api_call":
        async with aiohttp.ClientSession() as session:
            async with session.get(
                f"{config.api_endpoint}/data",
                headers={"Authorization": f"Bearer {config.api_key}"}
            ) as response:
                data = await response.json()
                return [TextContent(type="text", text=str(data))]

    raise ValueError(f"Unknown tool: {name}")

.env file:

API_KEY=your_api_key_here
API_ENDPOINT=https://api.example.com
MAX_RETRIES=3
DEBUG=true
CACHE_DIR=~/.cache/my-mcp-server

Load .env in development:

# At top of server.py
try:
    from dotenv import load_dotenv
    load_dotenv()
except ImportError:
    pass  # dotenv not required in production

</env_config>

Advanced Features

<caching> **Caching Pattern**:
from functools import lru_cache
from datetime import datetime, timedelta
from typing import Any

class Cache:
    """Simple time-based cache."""
    def __init__(self):
        self._cache: dict[str, tuple[Any, datetime]] = {}

    def get(self, key: str, ttl: int = 300) -> Any | None:
        """Get cached value if not expire