Complete guide for contributing to and extending the MCP server template.

Prerequisites

Development Environment

1. Clone and Setup

# Clone the repository
git clone https://github.com/aj-geddes/python-mcp-server-template.git
cd python-mcp-server-template

# Create virtual environment
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

# Install development dependencies
pip install -r requirements-dev.txt
pip install -r requirements.txt

2. Development Dependencies

# Core testing and development tools
pytest>=8.0.0
pytest-asyncio>=0.25.0
pytest-cov>=6.0.0
coverage[toml]>=7.6.0

# Code quality
mypy>=1.13.0
black>=24.0.0
isort>=5.13.0
flake8>=7.1.0

# Security tools
bandit[toml]>=1.7.0
safety>=3.2.0

# Optional enhanced tools
structlog>=24.4.0
prometheus-client>=0.21.0
limits>=3.13.0
rich>=13.9.0

3. IDE Configuration

VS Code Settings (.vscode/settings.json):

{
    "python.defaultInterpreterPath": "./venv/bin/python",
    "python.linting.enabled": true,
    "python.linting.mypyEnabled": true,
    "python.linting.banditEnabled": true,
    "python.formatting.provider": "black",
    "python.testing.pytestEnabled": true,
    "python.testing.pytestArgs": ["tests/"]
}

PyCharm Configuration:

Development Workflow

1. Running Tests

# Run all tests
pytest tests/ -v

# Run with coverage
pytest tests/ --cov=mcp_server --cov-report=html --cov-report=term

# Run specific test file
pytest tests/test_mcp_server.py -v

# Run tests matching pattern
pytest tests/ -k "test_health" -v

Coverage Goals:

2. Code Quality Checks

# Type checking with MyPy
mypy mcp_server/ --ignore-missing-imports

# Code formatting with Black
black mcp_server/ tests/

# Import sorting with isort
isort mcp_server/ tests/

# Linting with flake8
flake8 mcp_server/ tests/

# Security scan with Bandit
bandit -r mcp_server/ -f json

# Comprehensive security scan
python security_scan.py

3. Development Server

# Run in development mode with auto-reload
python -m mcp_server

# Run with HTTP transport for testing
MCP_TRANSPORT=http MCP_HOST=127.0.0.1 MCP_PORT=8080 python mcp_server.py

# Run with enhanced logging
PYTHONUNBUFFERED=1 python mcp_server.py

4. Docker Development

# Build development image
docker build -t mcp-server-dev .

# Run with volume mounting for live development
docker run -v $(pwd):/workspace -p 8080:8080 mcp-server-dev

# Run with docker-compose for full stack
docker-compose -f docker-compose.dev.yml up

Project Structure

python-mcp-server-template/
├── mcp_server/              # Main server package
│   ├── __init__.py         # Core server implementation
│   └── __main__.py         # Entry point module
├── tests/                  # Test suite
│   ├── test_mcp_server.py  # Main functionality tests
│   ├── test_server.py      # Server lifecycle tests
│   └── ...                 # Additional test modules
├── docs/                   # Documentation (GitHub Pages)
├── security_scan.py        # Automated security scanning
├── benchmark.py            # Performance benchmarking
├── monitoring.py           # Advanced monitoring tools
├── requirements*.txt       # Dependency specifications
├── pyproject.toml         # Project configuration
├── Dockerfile             # Production container
└── docker-compose.yml     # Multi-service orchestration

Adding New Tools

1. Basic Tool Implementation

# In mcp_server/__init__.py

@with_monitoring("my_new_tool")
async def my_new_tool_impl(param1: str, param2: int = 10) -> Dict[str, Any]:
    """Implementation function with monitoring."""
    try:
        # Your business logic here
        result = f"Processed {param1} with {param2}"
        
        return {
            "result": result,
            "status": "✅ Success",
            "timestamp": time.time()
        }
        
    except Exception as e:
        raise MCPError(f"Tool failed: {str(e)}")

@mcp.tool()
async def my_new_tool(param1: str, param2: int = 10) -> Dict[str, Any]:
    """User-facing tool function."""
    result: Dict[str, Any] = await my_new_tool_impl(param1, param2)
    return result

2. Tool Testing

# In tests/test_my_tool.py

import pytest
from mcp_server import my_new_tool_impl, MCPError

@pytest.mark.asyncio
async def test_my_new_tool_success():
    """Test successful tool execution."""
    result = await my_new_tool_impl("test", 5)
    
    assert result["status"] == "✅ Success"
    assert "test" in result["result"]
    assert result["timestamp"] > 0

@pytest.mark.asyncio
async def test_my_new_tool_error_handling():
    """Test error handling."""
    with pytest.raises(MCPError):
        await my_new_tool_impl("", -1)  # Invalid inputs

3. Security Considerations

Input Validation:

def validate_input(param: str) -> str:
    """Validate and sanitize inputs."""
    if not param or len(param.strip()) == 0:
        raise MCPError("Parameter cannot be empty")
    
    # Remove potentially dangerous characters
    sanitized = re.sub(r'[<>"]', '', param.strip())
    return sanitized

Path Operations:

async def secure_file_operation(file_path: str) -> Dict[str, Any]:
    """Always use validate_path for file operations."""
    try:
        validated_path = validate_path(file_path)
        # Safe to proceed with validated_path
        return {"path": str(validated_path)}
    except Exception as e:
        raise MCPError(f"Invalid path: {str(e)}")

Testing Guidelines

1. Test Structure

import pytest
from unittest.mock import patch, AsyncMock
from mcp_server import tool_function_impl, MCPError

class TestMyTool:
    """Test suite for my_tool functionality."""
    
    @pytest.mark.asyncio
    async def test_happy_path(self):
        """Test normal operation."""
        # Setup
        # Execute  
        # Assert
        
    @pytest.mark.asyncio
    async def test_error_conditions(self):
        """Test error handling."""
        # Test various error scenarios
        
    @pytest.mark.asyncio  
    async def test_edge_cases(self):
        """Test boundary conditions."""
        # Test edge cases and limits

2. Mocking External Dependencies

@patch('mcp_server.subprocess.run')
@pytest.mark.asyncio
async def test_command_execution(mock_run):
    """Test command execution with mocked subprocess."""
    # Mock the subprocess.run call
    mock_run.return_value.returncode = 0
    mock_run.return_value.stdout = "success"
    mock_run.return_value.stderr = ""
    
    result = await run_command_impl(["echo", "test"])
    assert result["success"] is True

3. Async Testing Patterns

@pytest.mark.asyncio
async def test_async_operations():
    """Test asynchronous operations properly."""
    # Use await for async functions
    result = await async_function()
    
    # Use AsyncMock for async mocks
    with patch('mcp_server.async_dep', new_callable=AsyncMock) as mock:
        mock.return_value = "mocked"
        result = await function_using_async_dep()
        mock.assert_called_once()

Performance Considerations

1. Benchmarking New Features

# Add benchmark for new tools
import time

@with_monitoring("performance_test")
async def benchmark_my_tool():
    """Benchmark tool performance."""
    start = time.time()
    
    # Run tool multiple times
    for _ in range(100):
        await my_new_tool_impl("test", 10)
    
    duration = time.time() - start
    return {
        "tool": "my_new_tool",
        "iterations": 100,
        "total_duration": duration,
        "avg_duration": duration / 100
    }

2. Memory Usage

import tracemalloc

async def test_memory_usage():
    """Test memory usage of tools."""
    tracemalloc.start()
    
    # Run your tool
    await my_new_tool_impl("test", 10)
    
    current, peak = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    
    assert peak < 10 * 1024 * 1024  # Less than 10MB

Security Testing

1. Input Validation Tests

@pytest.mark.asyncio
async def test_path_traversal_prevention():
    """Test directory traversal attack prevention."""
    dangerous_paths = [
        "../../../etc/passwd",
        "/etc/passwd", 
        "\\..\\..\\windows\\system32"
    ]
    
    for path in dangerous_paths:
        with pytest.raises(MCPError, match="outside allowed directory"):
            await read_file_impl(path)

2. Rate Limiting Tests

@patch('mcp_server.RATE_LIMITER')
@pytest.mark.asyncio
async def test_rate_limiting(mock_limiter):
    """Test rate limiting behavior."""
    mock_limiter.hit.return_value = False  # Simulate rate limit exceeded
    
    with pytest.raises(RateLimitExceeded):
        await my_tool_impl("test")

Contributing Guidelines

1. Code Standards

2. Commit Messages

# Format: <type>(<scope>): <description>
feat(tools): add new file compression tool
fix(security): resolve path validation vulnerability  
docs(api): update tool documentation
test(coverage): add missing test cases
security(scan): enhance automated security checks

3. Pull Request Process

  1. Create feature branch from master
  2. Implement changes with tests
  3. Run all checks: make check (or manual)
  4. Update documentation as needed
  5. Submit pull request with description
  6. Address review feedback
  7. Squash and merge when approved

4. Documentation Updates

When adding features:

Troubleshooting

Common Development Issues

Import Errors:

# Ensure virtual environment is activated
source venv/bin/activate
pip install -r requirements-dev.txt

Test Failures:

# Run specific failing test with verbose output
pytest tests/test_failing.py::test_function -vvs

# Check test dependencies
pip install pytest-asyncio

Type Errors:

# Install type stubs
pip install types-requests types-setuptools

# Run MyPy on specific file
mypy mcp_server/__init__.py --show-error-codes

Docker Issues:

# Rebuild without cache
docker build --no-cache -t mcp-server-dev .

# Check container logs
docker logs <container-id>

Support and Community


Happy coding! 🚀