← AI Agents Open Protocols
🤖 AI Agents

Building an MCP Server in Python

Source: mortalapps.com
TL;DR
  • An MCP server is a standardized interface that exposes tools, resources, and prompts to LLM clients via the Model Context Protocol.
  • It solves the problem of fragmented tool integration by providing a universal, transport-agnostic schema for agentic capabilities.
  • In production, MCP servers enable a 'write once, run anywhere' architecture for enterprise data connectors and internal APIs.
  • This tutorial results in a production-ready Python server supporting both stdio and Streamable HTTP (current recommended transport; SSE is a separate legacy/deprecated transport) transports.

Why This Matters

To build mcp server python components is to adopt a paradigm shift in how AI agents interact with external systems. Historically, developers were forced to write bespoke integration layers for every LLM framework - one for LangChain, another for Semantic Kernel, and a third for custom internal bots. This fragmentation created significant technical debt and maintenance overhead. The Model Context Protocol (MCP) was invented to decouple the 'intelligence' of the model from the 'capability' of the tools. By standardizing the communication layer, a single Python-based MCP server can simultaneously serve a local Claude Desktop instance, a cloud-hosted agentic workflow, and a fleet of autonomous IDE agents.

Ignoring this standard in production leads to 'integration silos' where valuable enterprise data is trapped behind proprietary wrappers that cannot be easily audited, secured, or reused across different model providers. From a production engineering perspective, MCP provides a structured way to enforce schema validation, rate limiting, and security boundaries at the edge of the tool execution environment. It allows teams to move away from granting LLMs broad API access and instead provide granular, well-defined functions. This approach is essential when building systems that must comply with strict governance frameworks, as it centralizes the logic for tool execution, logging, and access control into a single, testable service. Use this approach when you need to expose internal databases, legacy APIs, or specialized compute functions to any MCP-compliant agent without rewriting the interface for each new model or framework version.

Core Concepts

MCP Server Components

An MCP server is composed of several logical layers that handle the lifecycle of a request from an LLM client:

  • Transport Layer: The physical communication medium. Common types include stdio (standard input/output) for local processes and SSE (Server-Sent Events) for networked HTTP services.
  • Protocol Handler: The logic that parses JSON-RPC 2.0 messages, manages session state, and handles capability negotiation during the initialize handshake.
  • Tools: Executable functions that the LLM can invoke. Each tool must have a name, a description (used by the LLM for discovery), and a strict input schema.
  • Resources: Passive data sources (like files or database rows) that the LLM can read but not execute.
  • Prompts: Pre-defined templates that help the client structure interactions with the server's specific capabilities.

Transport Variants

Transport Use Case Latency Security Model
stdio Local IDE extensions, CLI tools Ultra-low Process-level isolation
SSE (HTTP) Remote agents, multi-tenant apps Moderate TLS, OAuth 2.1, API Keys

How It Works

The MCP Lifecycle

  1. Initialization: The client (e.g., Claude Desktop) starts the server process or connects via HTTP. They exchange initialize messages to negotiate capabilities. The server declares it supports tools and resources; the client declares its protocol version.
  2. Discovery: The client sends a tools/list request. The server responds with a list of available tools, including their Pydantic-generated JSON schemas and descriptions. This is how the LLM 'knows' what it can do.
  3. The Call-Response Loop: When the LLM decides to use a tool, the client sends a tools/call request containing the tool name and arguments. The server validates these arguments against the schema before execution.
  4. Execution and Error Handling: The server executes the Python function. If the function fails, the server must catch the exception and return a structured JSON-RPC error object rather than crashing. This allows the LLM to 'see' the error and potentially self-correct.
  5. Shutdown: The client sends a notifications/cancelled or simply closes the transport. The server must perform a clean exit, closing any open database connections or file handles.

Architecture

The architecture follows a classic Hub-and-Spoke model where the MCP Server acts as the spoke providing specialized capabilities to a central LLM Hub. Execution starts at the Client (Hub), which initiates a JSON-RPC 2.0 connection over a Transport (stdio or SSE). The message flows into the MCP Python SDK, which acts as a dispatcher. The dispatcher routes the request to the specific Tool Handler based on the method name. Data flows from the Tool Handler to external systems (Databases, APIs) and back. The result is wrapped in a Content object (Text, Image, or Resource) and sent back through the Transport to the Client. The system is designed to be stateless at the protocol level, though the underlying Python logic may maintain its own stateful connections to external resources.

Environment Setup and SDK Selection

To build mcp server python applications, the primary dependency is the mcp Python SDK. This library provides high-level abstractions for both the low-level JSON-RPC logic and the transport implementations. Start by creating a virtual environment and installing the core package along with uvicorn for HTTP support.

pip install mcp[cli] uvicorn pydantic

Defining Tools with Type Safety

The most critical part of an MCP server is the tool definition. The LLM relies entirely on the tool's description and parameter schema to understand when and how to call it. Using the FastMCP class simplifies this by automatically generating schemas from Python type hints and docstrings.

Implementing the Stdio Transport

For local development, stdio is the default. The server listens on stdin and writes to stdout. It is vital to redirect all application logs to stderr to avoid polluting the protocol stream. If your code prints a debug statement to stdout, the MCP client will fail to parse the resulting JSON-RPC message, leading to a connection drop.

Transitioning to Streamable HTTP (current recommended transport; SSE is a separate legacy/deprecated transport)

In production environments, you often need to host the MCP server as a web service. MCP uses Server-Sent Events (SSE) for the server-to-client stream and standard POST requests for client-to-server messages. This 'Streamable HTTP' approach allows for long-running tool executions without the overhead of full WebSockets.

Schema Validation and Constraints

Every tool input should be validated using Pydantic models. This prevents 'prompt injection' style attacks where an LLM might try to pass malformed data to your internal systems. By defining Field(..., ge=0, le=100) or regex patterns in your Pydantic models, you provide the LLM with hard constraints that improve the reliability of the agentic loop.

Handling Large Contexts and Resources

Resources in MCP allow you to expose large datasets without forcing the LLM to 'call' a tool. For example, a resource could be a URI like db://logs/today. When the client requests this resource, the server streams the content. This is more efficient than tool calls for bulk data transfer. Use the list_resources and read_resource handlers to implement this. Ensure you implement pagination for resources that could return megabytes of data, as most LLM context windows are still a bottleneck.

Code Example

A basic MCP server using the FastMCP abstraction for local stdio transport.
Python
import os
import logging
from mcp.server.fastmcp import FastMCP

# Initialize FastMCP server
mcp = FastMCP("WeatherService")

# Configure logging to stderr to avoid breaking stdio protocol
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@mcp.tool()
def get_weather(city: str) -> str:
    """Get the current weather for a specific city."""
    # In production, use os.getenv for API keys
    api_key = os.getenv("WEATHER_API_KEY")
    if not api_key:
        return "Error: API key not configured."
    
    logger.info(f"Fetching weather for {city}")
    # Mocking API call for demonstration
    return f"The weather in {city} is sunny, 22°C."

if __name__ == "__main__":
    # Run using stdio transport by default
    mcp.run()
Expected Output
The server starts and waits for JSON-RPC input on stdin. When queried by an MCP client, it returns weather data.
An advanced MCP server implementation using Starlette for SSE (HTTP) transport.
Python
from mcp.server import Server
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route

# Create low-level MCP server
server = Server("ProductionServer")

# Define tool handlers manually for granular control
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict):
    if name == "query_db":
        return [{"type": "text", "text": "Query results..."}]
    raise ValueError(f"Unknown tool: {name}")

# SSE Transport setup
sse = SseServerTransport("/messages")

async def handle_sse(request):
    async with sse.connect_scope(request.scope, request.receive, request.send) as scope:
        await server.run(
            scope.incoming,
            scope.outgoing,
            server.create_initialization_options()
        )

app = Starlette(routes=[
    Route("/sse", endpoint=handle_sse),
    Route("/messages", endpoint=sse.handle_post_messages, methods=["POST"])
])
Expected Output
A web server listening on port 8000, providing an MCP endpoint via SSE for remote agent connections.

Key Takeaways

MCP decouples LLM intelligence from tool execution, enabling cross-platform interoperability.
The Python SDK provides both high-level (FastMCP) and low-level (Server) abstractions for developers.
Stdio transport is optimized for local performance, while SSE is required for cloud-based agentic services.
Tool discovery relies on high-quality docstrings and Pydantic schemas to guide the LLM.
Production servers must redirect application logs to stderr to prevent protocol corruption.
Stateless design and async I/O are the keys to scaling MCP servers for enterprise workloads.

Frequently Asked Questions

What is the difference between stdio and SSE transport? +
Stdio uses the process's standard input/output for local communication, while SSE (Server-Sent Events) uses HTTP for remote, network-based communication.
How do I test my MCP server locally? +
Use the MCP Inspector tool (`npx @modelcontextprotocol/inspector`) which provides a web interface to manually trigger tools and view logs.
Can I use an MCP server with LangChain? +
Yes, LangChain and other frameworks have MCP clients that can connect to your server and automatically convert its tools into framework-native tools.
What happens if my Python tool function raises an exception? +
The MCP SDK catches the exception and returns a JSON-RPC error response to the client, which the LLM can then interpret.
How do I handle secrets like API keys in an MCP server? +
Use environment variables or a secret management service; never hardcode them in the tool logic.
Why is my MCP server not showing up in Claude Desktop? +
Check the Claude Desktop config file for path errors and ensure your server logs to stderr, as any stdout output will break the connection.
Does MCP support streaming tool outputs? +
The current spec primarily supports discrete responses, but you can use Resources or multiple tool calls to simulate streaming for large data.
Is MCP only for Python? +
No, MCP is a protocol. While this guide focuses on Python, there are SDKs for TypeScript, Go, and other languages.