Building MCP Server with Python

Building an MCP Server with Python: Beginner's Guide

event

The promise of AI assistants has always been their ability to help with real-world tasks, but there's been a fundamental limitation: most AI systems operate in isolation, unable to access the specific data sources, APIs, and tools that make them truly useful for your particular use case. Whether you need an AI to query your company's database, interact with your project management system, or access real-time data from specialized APIs, the traditional approach has required building custom integrations for each AI platform - a time-consuming and fragmented process.

This is where the Model Context Protocol (MCP) changes everything. Instead of AI assistants being limited to their training data or requiring complex custom integrations, MCP provides a standardized way to connect AI systems to your specific data sources and tools. Need your AI to access your customer database? Build an MCP server. Want it to interact with your inventory management API? Create an MCP server. The same server works across different AI platforms, eliminating the need to rebuild integrations for each system.

The adoption of MCP is accelerating rapidly across the AI ecosystem. Major platforms are embracing the protocol as the standard for AI integration. Claude Desktop and Claude for Code already provide native MCP support, allowing users to seamlessly connect to custom data sources and tools. The major AI API providers - OpenAI, Anthropic, and Google - are adding MCP compatibility to their completion APIs, enabling developers to build AI applications that can access external systems through standardized interfaces. This growing ecosystem means that MCP servers you build today will work with an expanding range of AI platforms and applications.

In this comprehensive tutorial, you'll learn how to build production-ready MCP servers using Python through an iterative, hands-on approach. We'll start with a minimal server and progressively add features, showing you exactly how to develop, test, and extend your server step by step. By the end of this guide, you'll have built a complete weather service MCP server with advanced features including AI-powered sampling, OAuth authentication, and production deployment capabilities.

Understanding MCP

The Model Context Protocol represents a paradigm shift in how we think about AI integration architecture. At its core, MCP is an open protocol that standardizes how applications provide context to large language models, but its implications extend far beyond simple data sharing.

To understand why MCP matters, consider the current state of AI integrations. Most AI applications today operate in isolation, with limited ability to access real-time data or interact with external systems. When developers want to give an AI assistant access to a database, file system, or web service, they typically need to build custom solutions that are tightly coupled to specific AI platforms. This approach creates several problems: vendor lock-in, duplicated effort across different AI platforms, security concerns from direct API access, and maintenance overhead as APIs evolve.

MCP addresses these challenges through a client-server architecture that introduces a standardized intermediary layer. Instead of AI applications directly accessing external systems, they communicate through MCP servers that act as secure, standardized gateways. This architecture provides several key benefits that make it particularly powerful for enterprise and production deployments.

Core MCP Architecture

At the heart of MCP's architecture are three core participants that work together to enable seamless AI integration:

MCP Host: The AI application that coordinates and manages connections to multiple MCP servers. Popular AI applications like Claude Desktop act as MCP hosts when they support the protocol.

MCP Client: A component within the host that maintains dedicated connections to individual MCP servers, handling protocol-level communication and connection lifecycle.

MCP Server: The component that exposes data and functionality to MCP clients in a standardized way. Servers can run locally (using STDIO transport) or remotely (using HTTP transport).

The Three Pillars of MCP

The protocol defines three fundamental primitives that servers can expose:

Tools are executable functions that AI applications can invoke to perform actions. These might include operations like querying a database, sending an email, or calling an external API.

Resources provide contextual information to AI applications without performing actions. They represent data that can be read and understood by the AI, such as file contents or database records.

Prompts are reusable templates that help structure interactions with language models, providing a way to encapsulate domain expertise and best practices.

Setting Up Your Development Environment

Before we start building our MCP server, let's set up a proper development environment that supports both rapid iteration and production deployment. We'll be using the Model Context Protocol Python SDK throughout this tutorial.

Prerequisites

Ensure you have Python 3.10 or higher installed on your system. You can check your Python version with:

python --version
# or
python3 --version

Creating Your Project

Create a new project directory and set up a virtual environment:

# Create project directory
mkdir weather-mcp-server
cd weather-mcp-server

# Create a virtual environment
python -m venv .venv

# Activate the virtual environment
# On macOS/Linux:
source .venv/bin/activate

# On Windows:
.venv\Scripts\activate

Installing Dependencies

Install the MCP Python SDK and additional dependencies using pip:

# Upgrade pip to the latest version
pip install --upgrade pip

# Install MCP SDK with CLI tools
pip install "mcp[cli]"

# Install HTTP client for API requests
pip install httpx

# Install JWT library for authentication (we'll use this later)
pip install PyJWT

# Install development dependencies
pip install pytest black isort mypy

Creating a Requirements File

Create a requirements.txt file to track your dependencies:

# Generate requirements file
pip freeze > requirements.txt

Your requirements.txt should include entries like:

mcp[cli]
httpx
PyJWT
pytest
black
isort
mypy

Building Your First MCP Server: Step by Step

Now let's build our weather MCP server iteratively, starting with the simplest possible implementation and adding features step by step. This approach helps you understand each component and makes debugging easier.

Step 1: Create a Minimal MCP Server

Let's start with the absolute minimum - a server that does nothing but respond to basic MCP protocol messages. Create a file called weather_server.py:

"""
Step 1: Minimal MCP server that responds to protocol messages
"""
from mcp.server.fastmcp import FastMCP

# Create the MCP server instance
mcp = FastMCP("weather-server")

if __name__ == "__main__":
    # Run the server using STDIO transport
    mcp.run(transport='stdio')

Test this minimal server:

# Start the MCP Inspector to test your server
python -m mcp dev weather_server.py

The MCP Inspector will start a web interface (typically at http://localhost:3000) where you can see that your server is running and responding to MCP protocol messages, even though it doesn't expose any tools yet.

Step 2: Add Your First Tool

Now let's add a simple tool that returns static weather information:

"""
Step 2: Add a simple weather tool with static data
"""
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather-server")

@mcp.tool()
def get_weather(city: str) -> str:
    """Get current weather for a city (demo with static data)"""
    # For now, return static data to test the tool mechanism
    return f"Weather in {city}: Sunny, 22°C (This is demo data)"

if __name__ == "__main__":
    mcp.run(transport='stdio')

Test the new tool in the MCP Inspector. You should now see a get_weather tool that you can call with different city names.

Step 3: Add Real API Integration

Now let's connect to a real weather API. We'll use the National Weather Service API, which is free and doesn't require authentication:

"""
Step 3: Connect to real weather API
"""
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather-server")

# Configuration
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-mcp-server/1.0"

async def make_nws_request(url: str) -> dict | None:
    """Make a request to the National Weather Service API"""
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json"
    }

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            print(f"API request failed: {e}", file=sys.stderr)
            return None

@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a specific location using coordinates"""
    # Step 1: Get the forecast grid endpoint for this location
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)

    if not points_data:
        return f"Unable to fetch forecast data for coordinates ({latitude}, {longitude})"

    # Step 2: Get the actual forecast data
    properties = points_data.get("properties", {})
    forecast_url = properties.get("forecast")

    if not forecast_url:
        return "Error: Unable to determine forecast URL for this location"

    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "Unable to fetch detailed forecast data"

    # Format the first few periods
    periods = forecast_data.get("properties", {}).get("periods", [])

    if not periods:
        return "No forecast periods available for this location"

    # Format the first 3 periods for display
    forecasts = []
    for period in periods[:3]:
        forecast_text = f"""
{period.get('name', 'Unknown Period')}:
Temperature: {period.get('temperature', 'Unknown')}°{period.get('temperatureUnit', 'F')}
Wind: {period.get('windSpeed', 'Unknown')} {period.get('windDirection', '')}
Forecast: {period.get('detailedForecast', 'No detailed forecast available')}
"""
        forecasts.append(forecast_text.strip())

    return f"Forecast for {latitude}, {longitude}:\n" + "\n---\n".join(forecasts)

if __name__ == "__main__":
    import sys
    mcp.run(transport='stdio')

Test this version with real coordinates (e.g., New York City: 40.7128, -74.0060). You should now get real weather forecast data!

Step 4: Add Input Validation and Error Handling

Let's make our server more robust by adding proper input validation and error handling:

"""
Step 4: Add input validation and better error handling
"""
import sys
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather-server")

# Configuration
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-mcp-server/1.0"

async def make_nws_request(url: str) -> dict | None:
    """Make a request to the National Weather Service API with proper error handling"""
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json"
    }

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except httpx.TimeoutException:
            print(f"Request timeout for URL: {url}", file=sys.stderr)
            return None
        except httpx.HTTPStatusError as e:
            print(f"HTTP error {e.response.status_code} for URL: {url}", file=sys.stderr)
            return None
        except Exception as e:
            print(f"Unexpected error for URL {url}: {e}", file=sys.stderr)
            return None

@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a specific location using coordinates"""
    # Validate coordinate ranges
    if not (-90 <= latitude <= 90):
        return "Error: Latitude must be between -90 and 90 degrees"

    if not (-180 <= longitude <= 180):
        return "Error: Longitude must be between -180 and 180 degrees"

    # Step 1: Get the forecast grid endpoint for this location
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)

    if not points_data:
        return f"Unable to fetch forecast data for coordinates ({latitude}, {longitude}). This location may be outside the US or the service may be unavailable."

    # Extract the forecast URL from the points response
    properties = points_data.get("properties", {})
    forecast_url = properties.get("forecast")

    if not forecast_url:
        return "Error: Unable to determine forecast URL for this location"

    # Step 2: Get the actual forecast data
    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "Unable to fetch detailed forecast data"

    # Extract and format forecast periods
    forecast_properties = forecast_data.get("properties", {})
    periods = forecast_properties.get("periods", [])

    if not periods:
        return "No forecast periods available for this location"

    # Format the first 3 periods for display
    forecasts = []
    for period in periods[:3]:
        forecast_text = f"""
{period.get('name', 'Unknown Period')}:
Temperature: {period.get('temperature', 'Unknown')}°{period.get('temperatureUnit', 'F')}
Wind: {period.get('windSpeed', 'Unknown')} {period.get('windDirection', '')}
Forecast: {period.get('detailedForecast', 'No detailed forecast available')}
"""
        forecasts.append(forecast_text.strip())

    location_info = f"Forecast for {latitude}, {longitude}:\n"
    return location_info + "\n---\n".join(forecasts)

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get active weather alerts for a US state"""
    # Validate state code format
    if not state or len(state) != 2:
        return "Error: Please provide a valid two-letter US state code (e.g., 'CA', 'NY', 'TX')"

    state = state.upper()
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"

    data = await make_nws_request(url)

    if not data:
        return f"Unable to fetch weather alerts for {state}. The service may be temporarily unavailable."

    features = data.get("features", [])

    if not features:
        return f"No active weather alerts for {state}."

    # Format alerts for display
    alerts = []
    for feature in features:
        props = feature.get("properties", {})
        event = props.get('event', 'Unknown Event')
        area = props.get('areaDesc', 'Unknown Area')
        severity = props.get('severity', 'Unknown Severity')
        description = props.get('description', 'No description available')

        alert_text = f"""
Event: {event}
Area: {area}
Severity: {severity}
Description: {description[:200]}{'...' if len(description) > 200 else ''}
"""
        alerts.append(alert_text.strip())

    alert_count = len(alerts)
    header = f"Found {alert_count} active weather alert{'s' if alert_count != 1 else ''} for {state}:\n"
    return header + "\n---\n".join(alerts)

if __name__ == "__main__":
    mcp.run(transport='stdio')

Now test both tools with various inputs, including invalid ones, to see how the error handling works.

Step 5: Add Resources for Contextual Information

Let's add resources that provide contextual information about weather stations and zones:

"""
Step 5: Add resources for contextual weather information
"""
import sys
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather-server")

# Configuration
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-mcp-server/1.0"

async def make_nws_request(url: str) -> dict | None:
    """Make a request to the National Weather Service API with proper error handling"""
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json"
    }

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except httpx.TimeoutException:
            print(f"Request timeout for URL: {url}", file=sys.stderr)
            return None
        except httpx.HTTPStatusError as e:
            print(f"HTTP error {e.response.status_code} for URL: {url}", file=sys.stderr)
            return None
        except Exception as e:
            print(f"Unexpected error for URL {url}: {e}", file=sys.stderr)
            return None

# Tools (same as Step 4)
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a specific location using coordinates"""
    # Validate coordinate ranges
    if not (-90 <= latitude <= 90):
        return "Error: Latitude must be between -90 and 90 degrees"

    if not (-180 <= longitude <= 180):
        return "Error: Longitude must be between -180 and 180 degrees"

    # Step 1: Get the forecast grid endpoint for this location
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)

    if not points_data:
        return f"Unable to fetch forecast data for coordinates ({latitude}, {longitude}). This location may be outside the US or the service may be unavailable."

    # Extract the forecast URL from the points response
    properties = points_data.get("properties", {})
    forecast_url = properties.get("forecast")

    if not forecast_url:
        return "Error: Unable to determine forecast URL for this location"

    # Step 2: Get the actual forecast data
    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "Unable to fetch detailed forecast data"

    # Extract and format forecast periods
    forecast_properties = forecast_data.get("properties", {})
    periods = forecast_properties.get("periods", [])

    if not periods:
        return "No forecast periods available for this location"

    # Format the first 3 periods for display
    forecasts = []
    for period in periods[:3]:
        forecast_text = f"""
{period.get('name', 'Unknown Period')}:
Temperature: {period.get('temperature', 'Unknown')}°{period.get('temperatureUnit', 'F')}
Wind: {period.get('windSpeed', 'Unknown')} {period.get('windDirection', '')}
Forecast: {period.get('detailedForecast', 'No detailed forecast available')}
"""
        forecasts.append(forecast_text.strip())

    location_info = f"Forecast for {latitude}, {longitude}:\n"
    return location_info + "\n---\n".join(forecasts)

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get active weather alerts for a US state"""
    # Validate state code format
    if not state or len(state) != 2:
        return "Error: Please provide a valid two-letter US state code (e.g., 'CA', 'NY', 'TX')"

    state = state.upper()
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"

    data = await make_nws_request(url)

    if not data:
        return f"Unable to fetch weather alerts for {state}. The service may be temporarily unavailable."

    features = data.get("features", [])

    if not features:
        return f"No active weather alerts for {state}."

    # Format alerts for display
    alerts = []
    for feature in features:
        props = feature.get("properties", {})
        event = props.get('event', 'Unknown Event')
        area = props.get('areaDesc', 'Unknown Area')
        severity = props.get('severity', 'Unknown Severity')
        description = props.get('description', 'No description available')

        alert_text = f"""
Event: {event}
Area: {area}
Severity: {severity}
Description: {description[:200]}{'...' if len(description) > 200 else ''}
"""
        alerts.append(alert_text.strip())

    alert_count = len(alerts)
    header = f"Found {alert_count} active weather alert{'s' if alert_count != 1 else ''} for {state}:\n"
    return header + "\n---\n".join(alerts)

# Resources for contextual information
@mcp.resource("weather://stations/{state}")
async def get_weather_stations(state: str) -> str:
    """Get information about weather observation stations in a state"""
    if not state or len(state) != 2:
        return "Error: Please provide a valid two-letter US state code"

    state = state.upper()
    url = f"{NWS_API_BASE}/stations?state={state}"

    data = await make_nws_request(url)

    if not data:
        return f"Unable to fetch weather station information for {state}"

    features = data.get("features", [])

    if not features:
        return f"No weather stations found for {state}"

    stations = []
    for feature in features[:10]:  # Limit to first 10 stations
        props = feature.get("properties", {})
        name = props.get("name", "Unknown Station")
        identifier = props.get("stationIdentifier", "Unknown ID")
        elevation = props.get("elevation", {}).get("value", "Unknown")

        stations.append(f"- {name} ({identifier}) - Elevation: {elevation}m")

    station_count = len(features)
    header = f"Weather stations in {state} (showing first 10 of {station_count}):\n"
    return header + "\n".join(stations)

if __name__ == "__main__":
    mcp.run(transport='stdio')

In the MCP Inspector, you should now see both tools and resources. Resources appear in a separate section and provide contextual information that AI applications can use to better understand the weather data.

Testing Your Server with MCP Inspector

Before integrating your server with AI assistants like Claude Desktop, it's essential to thoroughly test it using the MCP Inspector. The Inspector provides a web-based interface for testing MCP servers, allowing you to verify that all tools and resources work correctly.

Starting the MCP Inspector

To test your weather server with the MCP Inspector, run the following command from your project directory:

# Make sure you're in your project directory and virtual environment is activated
cd weather-mcp-server
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Start the MCP Inspector with your server
python -m mcp dev weather_server.py

This command will:

  1. Start your weather server in development mode
  2. Launch the MCP Inspector web interface
  3. Automatically connect the Inspector to your server

You should see output similar to:

Starting MCP Inspector...
Server running at: http://localhost:3000
MCP server connected successfully

Using the MCP Inspector Interface

Open your web browser and navigate to http://localhost:3000. The MCP Inspector interface provides several sections for testing your server:

Server Information Panel: Shows your server's name, version, and connection status. You should see "weather-server" listed as connected.

Tools Section: Lists all available tools with their descriptions and parameter schemas. For your weather server, you should see:

  • get_forecast - Get weather forecast for coordinates
  • get_alerts - Get active weather alerts for a US state
  • analyze_weather_trends - AI-powered weather analysis (if you've implemented Step 6)

Resources Section: Shows available resources that provide contextual information:

  • weather://stations/{state} - Weather station information for states

Testing Your Tools

Let's test each tool systematically:

Testing the Forecast Tool:

  1. Click on the get_forecast tool in the Inspector
  2. Enter test coordinates:
    • Latitude: 40.7128 (New York City)
    • Longitude: -74.0060
  3. Click "Execute Tool"
  4. Verify you receive a properly formatted weather forecast with temperature, wind, and detailed forecast information

Testing the Alerts Tool:

  1. Click on the get_alerts tool
  2. Enter a state code: CA (California)
  3. Click "Execute Tool"
  4. Check that you receive either active alerts or a "No active alerts" message

Testing Input Validation:

  1. Try invalid coordinates (e.g., latitude: 100, longitude: 200)
  2. Try invalid state codes (e.g., XYZ or California)
  3. Verify that your server returns appropriate error messages

Testing Resources

Testing Weather Stations Resource:

  1. Navigate to the Resources section
  2. Look for the weather://stations/{state} resource
  3. Click on it and enter a state code like TX
  4. Verify you receive a list of weather stations with names, identifiers, and elevations

Monitoring Server Logs

While testing, keep an eye on the terminal where you started the Inspector. You should see log messages showing:

  • Successful API requests to the National Weather Service
  • Any error messages or warnings
  • Tool execution confirmations

Example log output:

INFO: Tool 'get_forecast' called with params: {'latitude': 40.7128, 'longitude': -74.0060}
INFO: API request successful: https://api.weather.gov/points/40.7128,-74.0060
INFO: Forecast data retrieved successfully

Troubleshooting Common Issues

If you encounter problems during testing:

Server Won't Start:

  • Check that all dependencies are installed: pip install -r requirements.txt
  • Verify your virtual environment is activated
  • Look for syntax errors in your code

Tools Return Errors:

  • Check your internet connection (the server needs to access weather.gov)
  • Verify the National Weather Service API is accessible
  • Review error messages in the server logs

No Data Returned:

  • Try different coordinates (ensure they're within the US)
  • Check that state codes are valid two-letter abbreviations
  • Verify API responses aren't being blocked by firewalls

Validating Output Format

Ensure your tool outputs are properly formatted:

  • Weather forecasts should be human-readable
  • Alert information should include all relevant details
  • Error messages should be clear and actionable
  • All responses should be valid strings (JSON-serializable)

Once you've thoroughly tested your server with the MCP Inspector and confirmed that all tools and resources work correctly, you're ready to integrate it with AI assistants like Claude Desktop.

Registering Your Server with Claude Desktop

Now that you have a working MCP server, let's configure it to work with Claude Desktop. This involves editing Claude Desktop's configuration file to register your server.

Locating the Configuration File

The Claude Desktop configuration file is located at different paths depending on your operating system:

macOS:

~/Library/Application Support/Claude/claude_desktop_config.json

Windows:

%APPDATA%\Claude\claude_desktop_config.json

Linux:

~/.config/Claude/claude_desktop_config.json

Configuring Your Server

Create or edit the configuration file to include your weather server. Here's the basic configuration:

{
  "mcpServers": {
    "weather-server": {
      "command": "python",
      "args": ["weather_server.py"],
      "cwd": "/path/to/your/weather-mcp-server"
    }
  }
}

Replace /path/to/your/weather-mcp-server with the actual path to your project directory.

Alternative Configuration Using Virtual Environment

If you want to explicitly use your virtual environment's Python interpreter:

{
  "mcpServers": {
    "weather-server": {
      "command": "/path/to/your/weather-mcp-server/.venv/bin/python",
      "args": ["weather_server.py"],
      "cwd": "/path/to/your/weather-mcp-server"
    }
  }
}

On Windows, the path would be:

{
  "mcpServers": {
    "weather-server": {
      "command": "C:\\path\\to\\your\\weather-mcp-server\\.venv\\Scripts\\python.exe",
      "args": ["weather_server.py"],
      "cwd": "C:\\path\\to\\your\\weather-mcp-server"
    }
  }
}

Testing the Integration

  1. Save the configuration file
  2. Restart Claude Desktop completely (quit and reopen)
  3. Start a new conversation
  4. Try asking Claude to get weather information for a location

You should see Claude using your weather tools to provide real-time weather information!

Troubleshooting Claude Desktop Integration

If your server doesn't appear in Claude Desktop:

  1. Check the configuration file syntax - Use a JSON validator to ensure proper formatting
  2. Verify file paths - Make sure all paths in the configuration are absolute and correct
  3. Check permissions - Ensure Claude Desktop can execute your Python environment
  4. Review logs - Claude Desktop may show error messages in its interface
  5. Test with MCP Inspector first - Always verify your server works with the Inspector before configuring Claude Desktop

Adding Advanced Features

Now let's add some advanced features to make our server more powerful and production-ready.

Step 6: Add MCP Sampling

MCP sampling allows your server to request AI completions from the client, enabling intelligent analysis of weather data:

"""
Step 6: Add MCP sampling
"""
import sys
import httpx
from mcp.server.fastmcp import FastMCP, Context
from mcp.server.session import ServerSession

mcp = FastMCP("weather-server")

# Configuration
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-mcp-server/1.0"

async def make_nws_request(url: str) -> dict | None:
    """Make a request to the National Weather Service API with proper error handling"""
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json"
    }

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            print(f"API request failed: {e}", file=sys.stderr)
            return None

# Previous tools (get_forecast, get_alerts) - same as Step 5
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a specific location using coordinates"""
    # [Previous implementation from Step 5]
    # ... (keeping this concise for the tutorial)
    pass

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get active weather alerts for a US state"""
    # [Previous implementation from Step 5]
    # ... (keeping this concise for the tutorial)
    pass

# New AI-powered tool using sampling
@mcp.tool()
async def analyze_weather_trends(
    state: str,
    ctx: Context[ServerSession, None]
) -> str:
    """Analyze weather alert trends for a state using AI-powered analysis"""
    # First, gather current weather alert data
    alerts_data = await get_alerts(state)

    if "No active weather alerts" in alerts_data or "Error:" in alerts_data:
        return f"No weather alerts available for analysis in {state}"

    # Use sampling to analyze the weather data
    analysis_prompt = f"""
    Analyze the following weather alert data for {state} and provide insights about:

    1. The types of weather events currently affecting the region
    2. The severity and geographic distribution of alerts
    3. Potential impacts on daily activities and safety
    4. Any notable patterns or unusual weather conditions

    Weather Alert Data:
    {alerts_data}

    Provide a concise but comprehensive analysis that would be helpful for emergency management and public safety planning.
    """

    try:
        # Request AI analysis through sampling
        response = await ctx.request_sampling(
            messages=[{
                "role": "user",
                "content": {
                    "type": "text",
                    "text": analysis_prompt
                }
            }],
            modelPreferences={
                "intelligencePriority": 0.8,  # High intelligence for analysis
                "speedPriority": 0.4,         # Moderate speed requirement
                "costPriority": 0.3           # Cost is less important for analysis
            },
            systemPrompt="You are a meteorological analyst with expertise in weather pattern analysis and emergency management.",
            maxTokens=500
        )

        return f"Weather Trend Analysis for {state}:\n\n{response.content.text}"

    except Exception as e:
        return f"Unable to generate weather analysis: {str(e)}"

if __name__ == "__main__":
    mcp.run(transport='stdio')

Step 7: Add Authentication for Production Deployment

For production deployments, you'll want to add authentication. Here's how to add basic bearer token authentication:

"""
Step 7: Add authentication for production deployment
"""
import os
import sys
import httpx
import jwt
from datetime import datetime
from mcp.server.fastmcp import FastMCP, Context
from mcp.server.session import ServerSession

mcp = FastMCP("weather-server")

# Configuration
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-mcp-server/1.0"
JWT_SECRET = os.getenv("JWT_SECRET", "your-secret-key")

class AuthenticationError(Exception):
    """Custom exception for authentication failures"""
    pass

def verify_bearer_token(token: str) -> dict:
    """Verify and decode a JWT bearer token"""
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])

        # Check token expiration
        if datetime.utcnow().timestamp() > payload.get("exp", 0):
            raise AuthenticationError("Token has expired")

        return payload

    except jwt.InvalidTokenError as e:
        raise AuthenticationError(f"Invalid token: {str(e)}")

def require_authentication(func):
    """Decorator for tools that require authentication"""
    async def wrapper(*args, **kwargs):
        # In a real implementation, you'd extract the auth header from the request context
        # For this tutorial, we'll simulate authentication

        # Check if running in authenticated mode
        if os.getenv("REQUIRE_AUTH", "false").lower() == "true":
            # In production, extract token from request headers
            token = os.getenv("AUTH_TOKEN")
            if not token:
                return "Authentication required: Please provide a valid bearer token"

            try:
                user_context = verify_bearer_token(token)
                kwargs['user_context'] = user_context
            except AuthenticationError as e:
                return f"Authentication failed: {str(e)}"

        return await func(*args, **kwargs)

    return wrapper

# Previous API helper function
async def make_nws_request(url: str) -> dict | None:
    """Make a request to the National Weather Service API with proper error handling"""
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json"
    }

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            print(f"API request failed: {e}", file=sys.stderr)
            return None

# Authenticated tools
@mcp.tool()
@require_authentication
async def get_secure_forecast(
    latitude: float,
    longitude: float,
    user_context: dict = None
) -> str:
    """Get weather forecast with authentication and audit logging"""
    user_id = user_context.get("sub", "unknown") if user_context else "anonymous"
    print(f"Forecast request from user {user_id} for {latitude}, {longitude}", file=sys.stderr)

    # Use the same forecast logic as before
    # [Implementation details omitted for brevity]
    return f"Authenticated forecast for {latitude}, {longitude} (User: {user_id})"

if __name__ == "__main__":
    # Determine transport based on environment
    transport = os.getenv("MCP_TRANSPORT", "stdio")

    if transport == "http":
        # Production HTTP deployment with authentication
        port = int(os.getenv("PORT", 8000))
        host = os.getenv("HOST", "0.0.0.0")

        print(f"Starting secure MCP server on {host}:{port}", file=sys.stderr)
        mcp.run(transport="http", host=host, port=port)
    else:
        # Development STDIO deployment
        print("Starting MCP server in STDIO mode", file=sys.stderr)
        mcp.run(transport="stdio")

Production Deployment Considerations

When deploying your MCP server to production, consider these important factors:

Environment Configuration

Use environment variables for configuration:

# .env file for production
MCP_TRANSPORT=http
HOST=0.0.0.0
PORT=8000
REQUIRE_AUTH=true
JWT_SECRET=your-production-secret-key
NWS_API_USER_AGENT=your-production-app/1.0

Docker Deployment

Create a Dockerfile for containerized deployment:

FROM python:3.11-slim

WORKDIR /app

# Copy requirements and install dependencies
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# Copy project files
COPY weather_server.py ./

# Expose port
EXPOSE 8000

# Run the server
CMD ["python", "weather_server.py"]

Build and run the Docker container:

# Build the image
docker build -t weather-mcp-server .

# Run the container
docker run -p 8000:8000 -e MCP_TRANSPORT=http weather-mcp-server

Monitoring and Logging

Implement proper logging for production:

import logging
import sys

# Configure logging for production
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('/var/log/mcp-server.log'),
        logging.StreamHandler(sys.stderr)
    ]
)

logger = logging.getLogger(__name__)

# Use logger throughout your application
logger.info("Server starting up")
logger.error("API request failed", exc_info=True)

Troubleshooting Common Issues

STDIO Logging Problems

The most common issue is writing to stdout in STDIO servers:

# Wrong - breaks the protocol
print("Debug message")

# Correct - use stderr
print("Debug message", file=sys.stderr)

# Better - use logging
import logging
logging.basicConfig(stream=sys.stderr)
logger = logging.getLogger(__name__)
logger.info("Debug message")

JSON Serialization Errors

Ensure all tool return values are JSON-serializable:

# Wrong - returns complex object
@mcp.tool()
def bad_tool():
    return SomeComplexObject()

# Correct - returns string
@mcp.tool()
def good_tool():
    result = SomeComplexObject()
    return str(result)  # or result.to_dict() if available

Authentication Issues

For HTTP servers, verify your authentication configuration:

# Debug authentication setup
def debug_auth():
    required_vars = ["JWT_SECRET", "AUTH_TOKEN"]
    for var in required_vars:
        if not os.getenv(var):
            print(f"Missing environment variable: {var}", file=sys.stderr)

Next Steps and Advanced Topics

With your weather MCP server complete, you're ready to explore more advanced patterns:

Extending Your Server

Consider adding these features:

  • Historical weather data from additional APIs
  • Weather map integration with image resources
  • Real-time alerts using WebSocket connections
  • Machine learning predictions using sampling for analysis

Advanced Architectural Patterns

Explore these patterns for complex deployments:

  • Multi-server architectures with specialized domains
  • Server composition combining multiple MCP servers
  • Distributed deployments across cloud regions
  • Microservice integration with existing systems

References

[1] Anthropic's introduction to the Model Context Protocol

[2] Model Context Protocol documentation

[3] Model Context Protocol Python SDK

[4] Official MCP server building guide

[5] MCP architecture overview

[6] MCP sampling documentation

[7] MCP authorization specification