Close Menu

    Subscribe to Updates

    Get the latest news from tastytech.

    What's Hot

    Forget solid-state batteries – researchers have made a lithium-ion breakthrough that could boost range and drastically lower costs

    February 20, 2026

    Ubisoft Montpellier Evacuated 800 Employees Over A Bomb Threat

    February 20, 2026

    The Moment review – it’s not that easy being green

    February 20, 2026
    Facebook X (Twitter) Instagram
    Facebook X (Twitter) Instagram
    tastytech.intastytech.in
    Subscribe
    • AI News & Trends
    • Tech News
    • AI Tools
    • Business & Startups
    • Guides & Tutorials
    • Tech Reviews
    • Automobiles
    • Gaming
    • movies
    tastytech.intastytech.in
    Home»Business & Startups»FastMCP: The Pythonic Way to Build MCP Servers and Clients
    FastMCP: The Pythonic Way to Build MCP Servers and Clients
    Business & Startups

    FastMCP: The Pythonic Way to Build MCP Servers and Clients

    gvfx00@gmail.comBy gvfx00@gmail.comFebruary 20, 2026No Comments15 Mins Read
    Share
    Facebook Twitter LinkedIn Pinterest Email


    Image by Author

     

    Table of Contents

    Toggle
    • # Introduction
        • // Prerequisites
    • # What is the Model Context Protocol?
        • // Key Characteristics of MCP
        • // MCP Architecture: Servers and Clients
        • // Core Components of MCP
    • # What is FastMCP?
        • // FastMCP Philosophy
    • # Installation
    • # Building Your First MCP Server
        • // Step 1: Setting Up the Project Structure
        • // Step 2: Creating the MCP Server
        • // Step 3: Building the MCP Client
        • // Step 4: Running the Server and Client
    • # Advanced Patterns with FastMCP
    • # Conclusion
      • Related posts:
    • How I Built a Data Cleaning Pipeline Using One Messy DoorDash Dataset
    • How to Become a Data Analyst in 2026?
    • What’s on My Bookmarks Bar: Data Science Edition

    # 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 required navigating complex boilerplate code and detailed protocol specifications. FastMCP 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 build MCP servers and clients using FastMCP, which is comprehensive and complete with error handling, making it ideal for both beginners and intermediate developers.

     

    // Prerequisites

    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 is required for the CLI tools)
    • A code editor (I’m using VS Code, but you can use any editor of your choice)
    • Terminal/Command Line familiarity for running Python scripts

    It is also beneficial to have good Python programming knowledge (functions, decorators, type hints), some understanding of async/await syntax (optional, but helpful for advanced examples), familiarity with JSON and REST API concepts, and basic command-line terminal usage.

    Before FastMCP, building MCP servers required you to have 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 addresses these issues with intuitive decorators and a simple, Pythonic API, enabling you to focus on business logic rather than protocol implementation.

     

    # What is the Model Context Protocol?

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

     

    // Key Characteristics of MCP

    • Standardized Communication: Uses JSON-RPC 2.0 for reliable, structured messaging
    • Bidirectional: Supports both requests from clients to servers and responses back
    • Security: Built-in support for authentication and authorization patterns
    • Flexible Transport: 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
    Image by Author

     

    • MCP Server: Exposes capabilities (tools, resources, prompts) that external applications can use. Think of it as a backend API specifically designed for LLM integration.
    • MCP Client: Embedded in AI applications (like Claude Desktop, Cursor IDE, or custom applications) that connect to MCP servers to access their resources.

     

    // Core Components of MCP

    MCP servers expose three primary types of capabilities:

    1. Tools: Executable 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 fetch and use as context. Resources might be file contents, configuration data, or dynamically generated content.
    3. Prompts: Reusable message templates that guide LLM behavior. Prompts provide consistent instructions for multi-step 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. Created to reduce development headaches, FastMCP possesses the following characteristics:

    • Decorator-Based API: Python decorators (@mcp.tool, @mcp.resource, @mcp.prompt) eliminate boilerplate
    • Type Safety: Full type hints and validation using Python’s type system
    • Async/Await Support: Modern async Python for high-performance operations
    • Multiple Transports: Support for stdio, HTTP, WebSocket, and SSE
    • Built-in Testing: Easy client-server testing without subprocess complexity
    • Production Ready: Features like error handling, logging, and configuration for production deployments

     

    // FastMCP Philosophy

    FastMCP relies on three core principles:

    1. High-level abstractions: Less code and faster development cycles
    2. Simple: Minimal boilerplate allows focus on functionality over protocol details
    3. Pythonic: Natural Python idioms make it familiar to Python developers

     

    # Installation

     
    Start by installing FastMCP and the necessary 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')"

     

    # Building Your First MCP Server

     
    We will create a practical MCP server that demonstrates tools, resources, and prompts. We’ll build a Calculator Server that provides mathematical operations, configuration 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 navigate into your project folder:

     

    Initialize your project with the necessary files:

     

     

    // Step 2: Creating the MCP Server

    Our Calculator MCP Server is a simple MCP server demonstrating tools, resources, and prompts. 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 to stderr. The MCP protocol requires that all output, except protocol messages, be directed to stderr to avoid corrupting communication. The FastMCP(name="CalculatorServer") call creates the server instance. This 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 try-catch block to raise 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 decorated functions (@mcp.tool) expose mathematical operations. Each tool includes:

    • Type hints for parameters and return values
    • Comprehensive docstrings (MCP uses these as tool descriptions)
    • Error handling with try-except 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 decorated functions (@mcp.resource) provide static and dynamic data:

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

    Let’s build our prompts.

    @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)

     

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

    Error handling best practices included here are:

    • Specific exception catching (TypeError, ZeroDivisionError)
    • Meaningful error messages for users
    • Detailed logging for debugging
    • Graceful error propagation

     

    // Step 3: Building the MCP Client

    In this step, we will demonstrate how to interact with the Calculator MCP Server that 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 code above, the client uses async with Client(server) for safe connection management. This 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())

     

    Looking at the above code, before using tools, the client lists available capabilities. The await client.list_tools() gets all tool metadata, including descriptions. The await client.list_resources() discovers available resources. Lastly, the await client.list_prompts() will find available prompt templates.

    The await client.call_tool() 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 tool failures

    On the result extraction, the extract_tool_result() helper function unwraps MCP’s response format to get the actual value, handling both JSON and text responses.

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

    Lastly, the error handling catches tool errors (like division by zero) and logs them gracefully without crashing.

     

    // Step 4: Running the Server and Client

    You will open two terminals. On terminal 1, you will start the server:

    python calculator_server.py

     

    You should see:
     

    FastMCP Server terminal output
    Image by Author

     

    On terminal 2 run the client:

    python calculator_client.py

     

    Output will show:
     

    FastMCP Client terminal output
    Image by Author

     

    # Advanced Patterns with FastMCP

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

    • Asynchronous Operations: Use async def for tools that perform I/O-bound tasks like database queries or API calls
    • Dynamic Resources: Resources can accept arguments (e.g., resource://users/{user_id}) to fetch specific data points on the fly
    • Complex Type Validation: Use Pydantic models or complex Python type hints to ensure the LLM sends data in the exact format your backend requires
    • Custom Transports: While we used stdio, FastMCP also supports SSE (Server-Sent Events) for web-based integrations and custom UI tools

    # Conclusion

     
    FastMCP bridges the gap between the complex Model Context Protocol and the clean, decorator-based developer experience Python programmers expect. By removing the boilerplate associated with JSON-RPC 2.0 and manual transport management, it allows you to focus on what matters: building the tools that make LLMs more capable.

    In this tutorial, we covered:

    1. The core architecture of MCP (Servers vs. Clients)
    2. How to define Tools for actions, Resources for data, and Prompts for instructions
    3. How to build a functional client to test and chain your server logic

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

     
    What will you build next? Check out the FastMCP documentation to explore more advanced deployment strategies and UI integrations.
     
     

    Shittu Olumide is a software engineer and technical writer passionate about leveraging cutting-edge technologies to craft compelling narratives, with a keen eye for detail and a knack for simplifying complex concepts. You can also find Shittu on Twitter.



    Related posts:

    10 Nano Banana Pro Prompts that You Must Try!

    Probability Concepts You’ll Actually Use in Data Science

    A Comprehensive Comparison for Developers

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Previous ArticleApple inks deal for IMAX screenings of live Formula 1 races
    Next Article Indonesia, Morocco, Kosovo among 5 countries to send troops under Gaza plan | Gaza News
    gvfx00@gmail.com
    • Website

    Related Posts

    Business & Startups

    All About Google Colab File Management

    February 20, 2026
    Business & Startups

    A Hands-On Test of Google’s Newest AI

    February 20, 2026
    Business & Startups

    DBMS Data Models Explained: Types and SQL Examples

    February 19, 2026
    Add A Comment
    Leave A Reply Cancel Reply

    Top Posts

    BMW Will Put eFuel In Cars Made In Germany From 2028

    October 14, 202511 Views

    Best Sonic Lego Deals – Dr. Eggman’s Drillster Gets Big Price Cut

    December 16, 20259 Views

    What is Fine-Tuning? Your Ultimate Guide to Tailoring AI Models in 2025

    October 14, 20259 Views
    Stay In Touch
    • Facebook
    • YouTube
    • TikTok
    • WhatsApp
    • Twitter
    • Instagram

    Subscribe to Updates

    Get the latest tech news from tastytech.

    About Us
    About Us

    TastyTech.in brings you the latest AI, tech news, cybersecurity tips, and gadget insights all in one place. Stay informed, stay secure, and stay ahead with us!

    Most Popular

    BMW Will Put eFuel In Cars Made In Germany From 2028

    October 14, 202511 Views

    Best Sonic Lego Deals – Dr. Eggman’s Drillster Gets Big Price Cut

    December 16, 20259 Views

    What is Fine-Tuning? Your Ultimate Guide to Tailoring AI Models in 2025

    October 14, 20259 Views

    Subscribe to Updates

    Get the latest news from tastytech.

    Facebook X (Twitter) Instagram Pinterest
    • Homepage
    • About Us
    • Contact Us
    • Privacy Policy
    © 2026 TastyTech. Designed by TastyTech.

    Type above and press Enter to search. Press Esc to cancel.

    Ad Blocker Enabled!
    Ad Blocker Enabled!
    Our website is made possible by displaying online advertisements to our visitors. Please support us by disabling your Ad Blocker.