Fast MCP: A Python way to build MCP servers and clients

by SkillAiNest

Fast MCP: A Python way to build MCP servers and clientsPhoto by author

# Introduction

The Model Context Protocol (MCP) has changed how large language models (LLMs) interact with external tools, data sources, and services. However, building MCP servers from scratch traditionally requires complex boilerplate code and detailed protocol specifications. Fast MCP Eliminates this roadblock, providing a decorator-based, Pythonic framework that enables developers to build production-ready MCP servers and clients with minimal code.

In this tutorial, you’ll learn how to create MCP servers and clients using FastMCP, which is comprehensive and complete with error handling, making it ideal for both beginner and intermediate developers.

// Conditions

Before starting this tutorial, make sure you have:

  • Python 3.10 or higher (3.11+ recommended for better async performance)
  • pip or uv (Uv is recommended for FastMCP deployment and required for CLI tools)
  • A code editor (I’m using VS Code, but you can use any editor you like)
  • Familiarity with terminal/command line for running Python scripts

A good knowledge of Python programming (functions, decorators, type pointers), some understanding of async/await syntax (optional, but helpful for advanced examples), familiarity with JSON and REST API concepts, and using a basic command line terminal is also beneficial.

Before FastMCP, building MCP servers required a deep understanding of the MCP JSON-RPC specification, extensive boilerplate code for protocol handling, manual connection and transport management, and complex error handling and validation logic.

FastMCP solves these problems with intuitive decorators and a simple, Pythonic API, enabling you to focus on business logic rather than protocol implementation.

# What is Model Context Protocol?

The Model Context Protocol (MCP) is an open standard created by Anthropic. It provides a universal interface for AI applications to connect securely with external tools, data sources and services. MCP standardizes how LLMs interact with external systems, just as Web APIs standardize Web service communication.

// Key Features of MCP

  • Standard communication: JSON-RPC 2.0 is used for reliable, structured messaging.
  • bilateral: Supports both requests to servers and responses from clients.
  • Security: Built-in support for authentication and authorization patterns
  • Flexible transportation: Works with any transport mechanism (stdio, HTTP, WebSocket, SSE)

// MCP Architecture: Servers and Clients

MCP follows a clear client-server architecture:

MCP client-server architecture
Photo by author

  • MCP Server: Exposes capabilities (tools, resources, pointers) that can be used by external applications. Think of it as a backend API designed specifically for LLM integration.
  • MCP Client: Embedded AI applications (such as Cloud Desktop, Cursor IDE, or custom applications) that connect to MCP servers to access their resources.

// Basic components of MCP

MCP servers exhibit three basic types of capabilities:

  1. Tools: Actionable functions that LLMs can call to perform actions. Tools can query databases, call APIs, perform calculations, or trigger workflows.
  2. Resources: Read-only data that MCP clients can retrieve and use as context. Resources can be file content, configuration data, or dynamically generated content.
  3. Indications: Reusable message templates that guide LLM behavior. Indicators provide consistent instructions for multistep operations or specialized reasoning.

# What is FastMCP?

FastMCP is a high-level Python framework that simplifies the process of building both MCP servers and clients. Formulated to alleviate developing headaches, FastMCP has the following properties:

  • Decorator based API: python decorators (@mcp.tool, @mcp.resource, @mcp.prompt) remove the boiler plate.
  • Type of protection: Complete type notation and validation using Python’s type system
  • Async/await support: Advanced async Python for high performance operations
  • Multiple transports: Support for stdio, HTTP, WebSocket, and SSE
  • Built-in Testing: Simplified client-server testing without the complexity of sub-processes
  • Ready for production: Features like error handling, logging and configuration for production deployments

// The Fast MCP philosophy

FastMCP relies on three basic principles:

  1. High level abstraction: Less code and faster development cycles
  2. Simple: Allows functionality to be focused on minimal boilerplate protocol details.
  3. Python: Natural Python idioms make it familiar to Python developers.

# Installation

Start by installing FastMCP and the required dependencies. I recommend using uv.

If you don’t have UV, install it with pip:

Or install FastMCP directly with pip:

Verify that FastMCP is installed:

python -c "from fastmcp import FastMCP; print('FastMCP installed successfully')"

# Creating Your First MCP Server

We’ll build a practical MCP server that features tools, resources, and hints. We’ll build a calculator server that provides math operations, layout resources, and instruction prompts.

// Step 1: Setting up the project structure

We first need to create a project directory and initialize your environment. Create a folder for your project:

Then go to your project folder:

Start your project with the necessary files:

// Step 2: Creating the MCP Server

Our Calculator MCP Server is a simple MCP server that demonstrates tools, resources and indicators. Inside your project folder, create a file named calculator_server.py And add the following code.

import logging
import sys
from typing import Dict
from fastmcp import FastMCP

# Configure logging to stderr (critical for MCP protocol integrity)
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    stream=sys.stderr
)
logger = logging.getLogger(__name__)

# Create the FastMCP server instance
mcp = FastMCP(name="CalculatorServer")

The server imports FastMCP and configures logging. stderr. The MCP protocol requires that all output, except protocol messages, be sent to stderr to prevent communication corruption. gave FastMCP(name="CalculatorServer") Creates a call server instance. It handles all protocol management automatically.

Now, let’s create our tools.

@mcp.tool
def add(a: float, b: float) -> float:
    """
    Add two numbers together.
   
    Args:
        a: First number
        b: Second number
       
    Returns:
        Sum of a and b
    """
    try:
        result = a + b
        logger.info(f"Addition performed: {a} + {b} = {result}")
        return result
    except TypeError as e:
        logger.error(f"Type error in add: {e}")
        raise ValueError(f"Invalid input types: {e}")

@mcp.tool
def subtract(a: float, b: float) -> float:
    """
    Subtract b from a.
   
    Args:
        a: First number (minuend)
        b: Second number (subtrahend)
       
    Returns:
        Difference of a and b
    """
    try:
        result = a - b
        logger.info(f"Subtraction performed: {a} - {b} = {result}")
        return result
    except TypeError as e:
        logger.error(f"Type error in subtract: {e}")
        raise ValueError(f"Invalid input types: {e}")

We have defined functions for addition and subtraction. Both are wrapped in a trycatch block to handle value errors, log the information and return the result.

@mcp.tool
def multiply(a: float, b: float) -> float:
    """
    Multiply two numbers.
   
    Args:
        a: First number
        b: Second number
       
    Returns:
        Product of a and b
    """
    try:
        result = a * b
        logger.info(f"Multiplication performed: {a} * {b} = {result}")
        return result
    except TypeError as e:
        logger.error(f"Type error in multiply: {e}")
        raise ValueError(f"Invalid input types: {e}")

@mcp.tool
def divide(a: float, b: float) -> float:
    """
    Divide a by b.
   
    Args:
        a: Dividend (numerator)
        b: Divisor (denominator)
       
    Returns:
        Quotient of a divided by b
       
    Raises:
        ValueError: If attempting to divide by zero
    """
    try:
        if b == 0:
            logger.warning(f"Division by zero attempted: {a} / {b}")
            raise ValueError("Cannot divide by zero")
       
        result = a / b
        logger.info(f"Division performed: {a} / {b} = {result}")
        return result
    except (TypeError, ZeroDivisionError) as e:
        logger.error(f"Error in divide: {e}")
        raise ValueError(f"Division error: {e}")

Four decorative functions (@mcp.tool) expose arithmetic operations. Each tool includes:

  • Type prompts for parameters and return values.
  • Comprehensive documentation (MCP uses them as tool descriptions)
  • Error handling with try-save blocks.
  • Logging for debugging and monitoring
  • Input validation

Let’s move on to building resources.

@mcp.resource("config://calculator/settings")
def get_settings() -> Dict:
    """
    Provides calculator configuration and available operations.
   
    Returns:
        Dictionary containing calculator settings and metadata
    """
    logger.debug("Fetching calculator settings")
   
    return {
        "version": "1.0.0",
        "operations": ("add", "subtract", "multiply", "divide"),
        "precision": "IEEE 754 double precision",
        "max_value": 1.7976931348623157e+308,
        "min_value": -1.7976931348623157e+308,
        "supports_negative": True,
        "supports_decimals": True
    }

@mcp.resource("docs://calculator/guide")
def get_guide() -> str:
    """
    Provides a user guide for the calculator server.
   
    Returns:
        String containing usage guide and examples
    """
    logger.debug("Retrieving calculator guide")

    guide = """
       
    1. **add(a, b)**: Returns a + b
       Example: add(5, 3) = 8
   
    2. **subtract(a, b)**: Returns a - b
       Example: subtract(10, 4) = 6
   
    3. **multiply(a, b)**: Returns a * b
       Example: multiply(7, 6) = 42
   
    4. **divide(a, b)**: Returns a / b
       Example: divide(20, 4) = 5.0
   
    ## Error Handling
   
    - Division by zero will raise a ValueError
    - Non-numeric inputs will raise a ValueError
    - All inputs should be valid numbers (int or float)
   
    ## Precision
   
    The calculator uses IEEE 754 double precision floating-point arithmetic.
    Results may contain minor rounding errors for some operations.
    """
   
    return guide

Two decorative functions (@mcp.resource) provide static and dynamic data:

  • config://calculator/settings: Returns metadata about the calculator.
  • docs://calculator/guide: Returns the formatted user guide.
  • The URI format distinguishes resource types (convention: type://category/resource)

Let’s prepare our tips.

@mcp.prompt
def calculate_expression(expression: str) -> str:
    """
    Provides instructions for evaluating a mathematical expression.
    Args:
        expression: A mathematical expression to evaluate        
    Returns:
        Formatted prompt instructing the LLM how to evaluate the expression
    """
    logger.debug(f"Generating calculation prompt for: {expression}")

    prompt = f"""
    Please evaluate the following mathematical expression step by step:
   
    Expression: {expression}
   
    Instructions:
    1. Break down the expression into individual operations
    2. Use the appropriate calculator tool for each operation
    3. Follow order of operations (parentheses, multiplication/division, addition/subtraction)
    4. Show all intermediate steps
    5. Provide the final result
   
    Available tools: add, subtract, multiply, divide
    """
   
    return prompt.strip()

Finally, add the server startup script.

if __name__ == "__main__":
    logger.info("Starting Calculator MCP Server...")
   
    try:
        # Run the server with stdio transport (default for Claude Desktop)
        mcp.run(transport="stdio")
    except KeyboardInterrupt:
        logger.info("Server interrupted by user")
        sys.exit(0)
    except Exception as e:
        logger.error(f"Fatal error: {e}", exc_info=True)
        sys.exit(1)

gave @mcp.prompt The decorator creates instruction templates that guide LLM behavior for complex operations.

Best practices for dealing with errors include:

  • Caching specific exceptions (TypeError, ZeroDivisionError)
  • Error messages that make sense to users
  • Detailed logging for debugging
  • Preaching the graceful error

// Step 3: Creating the MCP Client

In this step, we will show how to interact with the calculator MCP server we created above. Create a new file named calculator_client.py.

import asyncio
import logging
import sys
from typing import Any
from fastmcp import Client, FastMCP

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    stream=sys.stderr
)
logger = logging.getLogger(__name__)

async def main():
    """
    Main client function demonstrating server interaction.
    """
   
    from calculator_server import mcp as server
   
    logger.info("Initializing Calculator Client...")

    try:
        async with Client(server) as client:
            logger.info("✓ Connected to Calculator Server")
           
            # DISCOVER CAPABILITIEs            
            print("\n" + "="*60)
            print("1. DISCOVERING SERVER CAPABILITIES")
            print("="*60)
           
            # List available tools
            tools = await client.list_tools()
            print(f"\nAvailable Tools ({len(tools)}):")
            for tool in tools:
                print(f"  • {tool.name}: {tool.description}")
           
            # List available resources
            resources = await client.list_resources()
            print(f"\nAvailable Resources ({len(resources)}):")
            for resource in resources:
                print(f"  • {resource.uri}: {resource.name or resource.uri}")
           
            # List available prompts
            prompts = await client.list_prompts()
            print(f"\nAvailable Prompts ({len(prompts)}):")
            for prompt in prompts:
                print(f"  • {prompt.name}: {prompt.description}")
           
            # CALL TOOLS
           
            print("\n" + "="*60)
            print("2. CALLING TOOLS")
            print("="*60)
           
            # Simple addition
            print("\nTest 1: Adding 15 + 27")
            result = await client.call_tool("add", {"a": 15, "b": 27})
            result_value = extract_tool_result(result)
            print(f"  Result: 15 + 27 = {result_value}")
           
            # Division with error handling
            print("\nTest 2: Dividing 100 / 5")
            result = await client.call_tool("divide", {"a": 100, "b": 5})
            result_value = extract_tool_result(result)
            print(f"  Result: 100 / 5 = {result_value}")
           
            # Error case: division by zero
            print("\nTest 3: Division by Zero (Error Handling)")
            try:
                result = await client.call_tool("divide", {"a": 10, "b": 0})
                print(f"  Unexpected success: {result}")
            except Exception as e:
                print(f"  ✓ Error caught correctly: {str(e)}")
           
            # READ RESOURCES
            print("\n" + "="*60)
            print("3. READING RESOURCES")
            print("="*60)
           
            # Read settings resource
            print("\nFetching Calculator Settings...")
            settings_resource = await client.read_resource("config://calculator/settings")
            print(f"  Version: {settings_resource(0).text}")
           
            # Read guide resource
            print("\nFetching Calculator Guide...")
            guide_resource = await client.read_resource("docs://calculator/guide")
            # Print first 200 characters of guide
            guide_text = guide_resource(0).text(:200) + "..."
            print(f"  {guide_text}")
           
            # CHAINING OPERATIONS
           
            print("\n" + "="*60)
            print("4. CHAINING MULTIPLE OPERATIONS")
            print("="*60)
           
            # Calculate: (10 + 5) * 3 - 7
            print("\nCalculating: (10 + 5) * 3 - 7")
           
            # Step 1: Add
            print("  Step 1: Add 10 + 5")
            add_result = await client.call_tool("add", {"a": 10, "b": 5})
            step1 = extract_tool_result(add_result)
            print(f"    Result: {step1}")
           
            # Step 2: Multiply
            print("  Step 2: Multiply 15 * 3")
            mult_result = await client.call_tool("multiply", {"a": step1, "b": 3})
            step2 = extract_tool_result(mult_result)
            print(f"    Result: {step2}")
           
            # Step 3: Subtract
            print("  Step 3: Subtract 45 - 7")
            final_result = await client.call_tool("subtract", {"a": step2, "b": 7})
            final = extract_tool_result(final_result)
            print(f"    Final Result: {final}")
           
            # GET PROMPT TEMPLATE
           
            print("\n" + "="*60)
            print("5. USING PROMPT TEMPLATES")
            print("="*60)
           
            expression = "25 * 4 + 10 / 2"
            print(f"\nPrompt Template for: {expression}")
            prompt_response = await client.get_prompt(
                "calculate_expression",
                {"expression": expression}
            )
            print(f"  Template:\n{prompt_response.messages(0).content.text}")
           
            logger.info("✓ Client operations completed successfully")
   
    except Exception as e:
        logger.error(f"Client error: {e}", exc_info=True)
        sys.exit(1)

From the above code, the client uses async with Client(server) To manage secure connections. It automatically handles connection setup and cleanup.

We also need a helper function to handle the results.

def extract_tool_result(response: Any) -> Any:
    """
    Extract the actual result value from a tool response.
   
    MCP wraps results in content objects, this helper unwraps them.
    """
    try:
        if hasattr(response, 'content') and response.content:
            content = response.content(0)
            # Prefer explicit text content when available (TextContent)
            if hasattr(content, 'text') and content.text is not None:
                # If the text is JSON, try to parse and extract a `result` field
                import json as _json
                text_val = content.text
                try:
                    parsed_text = _json.loads(text_val)
                    # If JSON contains a result field, return it
                    if isinstance(parsed_text, dict) and 'result' in parsed_text:
                        return parsed_text.get('result')
                    return parsed_text
                except _json.JSONDecodeError:
                    # Try to convert plain text to number
                    try:
                        if '.' in text_val:
                            return float(text_val)
                        return int(text_val)
                    except Exception:
                        return text_val

            # Try to extract JSON result via model `.json()` or dict-like `.json`
            if hasattr(content, 'json'):
                try:
                    if callable(content.json):
                        json_str = content.json()
                        import json as _json
                        try:
                            parsed = _json.loads(json_str)
                        except _json.JSONDecodeError:
                            return json_str
                    else:
                        parsed = content.json

                    # If parsed is a dict, try common shapes
                    if isinstance(parsed, dict):
                        # If nested result exists
                        if 'result' in parsed:
                            res = parsed.get('result')
                        elif 'text' in parsed:
                            res = parsed.get('text')
                        else:
                            res = parsed

                        # If res is str that looks like a number, convert
                        if isinstance(res, str):
                            try:
                                if '.' in res:
                                    return float(res)
                                return int(res)
                            except Exception:
                                return res
                        return res

                    return parsed
                except Exception:
                    pass
        return response
    except Exception as e:
        logger.warning(f"Could not extract result: {e}")
        return response


if __name__ == "__main__":
    logger.info("Calculator Client Starting...")
    asyncio.run(main())

Given the code above, before using the tools, the client lists the available capabilities. gave await client.list_tools() Gets all tool metadata, including description. gave await client.list_resources() Detects available resources. Finally, await client.list_prompts() Will find quick templates available.

gave await client.call_tool() The method does the following:

  • Takes the tool name and parameters as a dictionary.
  • Returns a wrapped Response object containing the result.
  • Integrates with error handling for device failures.

Upon conclusion, extract_tool_result() The helper function unpacks the MCP’s response format to get the actual value, handling both JSON and text responses.

The chaining operations you see above show how to use output from one tool as input to another tool, enabling complex calculations across multiple tool calls.

Finally, error handling catches tool errors (such as division by zero) and logs them gracefully without crashing.

// Step 4: Running the server and client

You will open two terminals. At Terminal 1, you would start the server:

python calculator_server.py

You should see:

Fast MCP Server terminal output
Photo by author

Run the client on terminal 2:

python calculator_client.py

The output will show:

Fast MCP client terminal output
Photo by author

# Advanced Patterns with Fast MCP

While our calculator example uses basic logic, FastMCP is designed to handle complex, production-ready scenarios. As you scale your MCP servers, you can benefit from:

  • Asynchronous Operations: use async def For tools that perform I/O-bound tasks such as database queries or API calls.
  • Dynamic Resources: Resources can accept arguments (for example, resource://users/{user_id}) to obtain specific data points on the fly
  • Complex type validation: Use Pydantic models or complex Python type prompts to ensure that LLM sends data in the exact format your backend needs.
  • Custom transportation: While we used to. stdioFastMCP also supports SSE (Server-Sent Events) for web-based integration and custom UI tools.

# The result

FastMCP bridges the gap between the complex model context protocol and the clean, decorator-based developer experience that Python programmers expect. By removing the boilerplate associated with JSON-RPC 2.0 and manual transport management, it allows you to focus on the important things: Building tools that empower LLMs.

In this tutorial, we covered:

  1. Basic Architecture of MCP (Server vs. Clients)
  2. How to define tools For actions, Resources For data, and Indicates. For instructions
  3. How to create a functional client to test and chain your server logic.

Whether you’re building a simple utility or a complex data orchestration layer, FastMCP provides the most “Pythonic” path to a production-ready agent ecosystem.

What will you build next? check Fast MCP documentation To explore more advanced deployment strategies and UI integration.

Shatu Olomide A software engineer and technical writer with a knack for simplifying complex concepts and a keen eye for detail, passionate about leveraging modern technology to craft compelling narratives. You can also search on Shittu. Twitter.

You may also like

Leave a Comment

At Skillainest, we believe the future belongs to those who embrace AI, upgrade their skills, and stay ahead of the curve.

Get latest news

Subscribe my Newsletter for new blog posts, tips & new photos. Let's stay updated!

@2025 Skillainest.Designed and Developed by Pro